Compare commits

..

20 Commits

Author SHA1 Message Date
syuilo 9059c149dd 2.38.3 2018-06-13 05:40:27 +09:00
syuilo 7d8e70b2ac Fix bug 2018-06-13 05:40:12 +09:00
syuilo 89105f5641 2.38.2 2018-06-13 05:25:27 +09:00
syuilo 1813d17b4c Fix bug 2018-06-13 05:24:44 +09:00
syuilo ce27b36fd0 Fix bug 2018-06-13 05:21:55 +09:00
syuilo e635a87628 2.38.1 2018-06-13 05:16:53 +09:00
syuilo 80c52433cc Fix bug 2018-06-13 05:15:26 +09:00
syuilo 1472f0b141 Fix #1712 2018-06-13 05:11:55 +09:00
syuilo 4d914f5c0a 2.38.0 2018-06-12 19:07:36 +09:00
syuilo 0318f7344f Fix bug 2018-06-12 19:05:40 +09:00
syuilo 413fbb3d0c モバイルでもハッシュタグを検索できるように 2018-06-12 19:03:57 +09:00
syuilo 8bc47baf4f #1710 2018-06-12 18:54:36 +09:00
syuilo e3f6d42a47 Fix bug 2018-06-12 11:38:44 +09:00
syuilo 8230935fd3 🎨 2018-06-12 11:27:35 +09:00
syuilo f968d05ea0 2.37.7 2018-06-12 09:10:52 +09:00
syuilo d6e5dc2167 Fix bugs 2018-06-12 09:10:34 +09:00
syuilo 460147fea2 2.37.6 2018-06-12 08:59:36 +09:00
syuilo cea44834bb Improve usability 2018-06-12 08:58:50 +09:00
syuilo 1af50fd7b8 冗長なハッシュタグの表示を無くした 2018-06-12 08:43:48 +09:00
syuilo b18013025f 🎨 2018-06-12 08:09:27 +09:00
17 changed files with 242 additions and 77 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "2.37.5", "version": "2.38.3",
"clientVersion": "1.0.6468", "clientVersion": "1.0.6486",
"codename": "nighthike", "codename": "nighthike",
"main": "./built/index.js", "main": "./built/index.js",
"private": true, "private": true,
@@ -40,6 +40,17 @@ export default Vue.component('mk-note-html', {
ast = this.ast; ast = this.ast;
} }
if (ast.filter(x => x.type != 'hashtag').length == 0) {
return;
}
while (ast[ast.length - 1] && (
ast[ast.length - 1].type == 'hashtag' ||
(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == ' ') ||
(ast[ast.length - 1].type == 'text' && ast[ast.length - 1].content == '\n'))) {
ast.pop();
}
// Parse ast to DOM // Parse ast to DOM
const els = flatten(ast.map(token => { const els = flatten(ast.map(token => {
switch (token.type) { switch (token.type) {
@@ -92,7 +103,7 @@ export default Vue.component('mk-note-html', {
case 'hashtag': case 'hashtag':
return createElement('a', { return createElement('a', {
attrs: { attrs: {
href: `${url}/search?q=${token.content}`, href: `${url}/tags/${token.hashtag}`,
target: '_blank' target: '_blank'
} }
}, token.content); }, token.content);
@@ -68,7 +68,7 @@ root(isDark)
> .fetching > .fetching
> .empty > .empty
margin 0 margin 0
padding 12px 16px padding 16px
text-align center text-align center
color #aaa color #aaa
@@ -76,18 +76,13 @@ root(isDark)
margin-right 4px margin-right 4px
> div > div
.chart-enter .chart-move
.chart-leave-to transition transform 1s ease
opacity 0
transform translateY(-30px)
> *
transition transform .3s ease, opacity .3s ease
> div > div
display flex display flex
align-items center align-items center
padding 16px padding 14px 16px
&:not(:last-child) &:not(:last-child)
border-bottom solid 1px isDark ? #393f4f : #eee border-bottom solid 1px isDark ? #393f4f : #eee
@@ -50,6 +50,7 @@ import * as XDraggable from 'vuedraggable';
import getKao from '../../../common/scripts/get-kao'; import getKao from '../../../common/scripts/get-kao';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import parse from '../../../../../text/parse'; import parse from '../../../../../text/parse';
import { host } from '../../../config';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@@ -129,6 +130,7 @@ export default Vue.extend({
// 自分は除外 // 自分は除外
if (this.$store.state.i.username == x.username && x.host == null) return; if (this.$store.state.i.username == x.username && x.host == null) return;
if (this.$store.state.i.username == x.username && x.host == host) return;
// 重複は除外 // 重複は除外
if (this.text.indexOf(`${mention} `) != -1) return; if (this.text.indexOf(`${mention} `) != -1) return;
@@ -5,7 +5,7 @@
<div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq"> <div class="gqpwvtwtprsbmnssnbicggtwqhmylhnq">
<template v-if="edit"> <template v-if="edit">
<header> <header>
<select v-model="widgetAdderSelected"> <select v-model="widgetAdderSelected" @change="addWidget">
<option value="profile">%i18n:common.widgets.profile%</option> <option value="profile">%i18n:common.widgets.profile%</option>
<option value="analog-clock">%i18n:common.widgets.analog-clock%</option> <option value="analog-clock">%i18n:common.widgets.analog-clock%</option>
<option value="calendar">%i18n:common.widgets.calendar%</option> <option value="calendar">%i18n:common.widgets.calendar%</option>
@@ -30,20 +30,15 @@
<option value="nav">%i18n:common.widgets.nav%</option> <option value="nav">%i18n:common.widgets.nav%</option>
<option value="tips">%i18n:common.widgets.tips%</option> <option value="tips">%i18n:common.widgets.tips%</option>
</select> </select>
<button @click="addWidget">%i18n:@add%</button>
</header> </header>
<x-draggable <x-draggable
:list="column.widgets" :list="column.widgets"
:options="{ handle: '.handle', animation: 150 }" :options="{ animation: 150 }"
@sort="onWidgetSort" @sort="onWidgetSort"
> >
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id"> <div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @contextmenu.stop.prevent="widgetFunc(widget.id)">
<header> <button class="remove" @click="removeWidget(widget)">%fa:times%</button>
<span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button> <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
</header>
<div @click="widgetFunc(widget.id)">
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" platform="deck"/>
</div>
</div> </div>
</x-draggable> </x-draggable>
</template> </template>
@@ -142,6 +137,13 @@ export default Vue.extend({
root(isDark) root(isDark)
.gqpwvtwtprsbmnssnbicggtwqhmylhnq .gqpwvtwtprsbmnssnbicggtwqhmylhnq
> header
padding 16px
> *
width 100%
padding 4px
.widget, .customize-container .widget, .customize-container
margin 8px margin 8px
@@ -149,7 +151,21 @@ root(isDark)
margin-top 0 margin-top 0
.customize-container .customize-container
background #fff cursor move
> *:not(.remove)
pointer-events none
> .remove
position absolute
z-index 1
top 8px
right 8px
width 32px
height 32px
color #fff
background rgba(#000, 0.7)
border-radius 4px
> header > header
color isDark ? #fff : #000 color isDark ? #fff : #000
+2
View File
@@ -42,6 +42,7 @@ import MkUserLists from './views/pages/user-lists.vue';
import MkUserList from './views/pages/user-list.vue'; import MkUserList from './views/pages/user-list.vue';
import MkSettings from './views/pages/settings.vue'; import MkSettings from './views/pages/settings.vue';
import MkOthello from './views/pages/othello.vue'; import MkOthello from './views/pages/othello.vue';
import MkTag from './views/pages/tag.vue';
Vue.use(MdCard); Vue.use(MdCard);
Vue.use(MdButton); Vue.use(MdButton);
@@ -88,6 +89,7 @@ init((launch) => {
{ path: '/i/drive/file/:file', component: MkDrive }, { path: '/i/drive/file/:file', component: MkDrive },
{ path: '/selectdrive', component: MkSelectDrive }, { path: '/selectdrive', component: MkSelectDrive },
{ path: '/search', component: MkSearch }, { path: '/search', component: MkSearch },
{ path: '/tags/:tag', component: MkTag },
{ path: '/othello', name: 'othello', component: MkOthello }, { path: '/othello', name: 'othello', component: MkOthello },
{ path: '/othello/:game', component: MkOthello }, { path: '/othello/:game', component: MkOthello },
{ path: '/@:user', component: MkUser }, { path: '/@:user', component: MkUser },
@@ -46,6 +46,7 @@ import * as XDraggable from 'vuedraggable';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import getKao from '../../../common/scripts/get-kao'; import getKao from '../../../common/scripts/get-kao';
import parse from '../../../../../text/parse'; import parse from '../../../../../text/parse';
import { host } from '../../../config';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@@ -123,6 +124,7 @@ export default Vue.extend({
// 自分は除外 // 自分は除外
if (this.$store.state.i.username == x.username && x.host == null) return; if (this.$store.state.i.username == x.username && x.host == null) return;
if (this.$store.state.i.username == x.username && x.host == host) return;
// 重複は除外 // 重複は除外
if (this.text.indexOf(`${mention} `) != -1) return; if (this.text.indexOf(`${mention} `) != -1) return;
+81
View File
@@ -0,0 +1,81 @@
<template>
<mk-ui>
<span slot="header">%fa:hashtag%{{ $route.params.tag }}</span>
<main>
<p v-if="!fetching && empty">%fa:search%{{ q }}に関する投稿は見つかりませんでした</p>
<mk-notes ref="timeline" :more="existMore ? more : null"/>
</main>
</mk-ui>
</template>
<script lang="ts">
import Vue from 'vue';
import Progress from '../../../common/scripts/loading';
const limit = 20;
export default Vue.extend({
data() {
return {
fetching: true,
moreFetching: false,
existMore: false,
offset: 0,
empty: false
};
},
watch: {
$route: 'fetch'
},
mounted() {
this.$nextTick(() => {
this.fetch();
});
},
methods: {
fetch() {
this.fetching = true;
Progress.start();
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api('notes/search_by_tag', {
limit: limit + 1,
offset: this.offset,
tag: this.$route.params.tag
}).then(notes => {
if (notes.length == 0) this.empty = true;
if (notes.length == limit + 1) {
notes.pop();
this.existMore = true;
}
res(notes);
this.fetching = false;
Progress.done();
}, rej);
}));
},
more() {
this.offset += limit;
const promise = (this as any).api('notes/search_by_tag', {
limit: limit + 1,
offset: this.offset,
tag: this.$route.params.tag
});
promise.then(notes => {
if (notes.length == limit + 1) {
notes.pop();
} else {
this.existMore = false;
}
notes.forEach(n => (this.$refs.timeline as any).append(n));
this.moreFetching = false;
});
return promise;
}
}
});
</script>
+6 -1
View File
@@ -48,6 +48,11 @@ export type INote = {
repliesCount: number; repliesCount: number;
reactionCounts: any; reactionCounts: any;
mentions: mongo.ObjectID[]; mentions: mongo.ObjectID[];
mentionedRemoteUsers: Array<{
uri: string;
username: string;
host: string;
}>;
/** /**
* public ... 公開 * public ... 公開
@@ -289,7 +294,7 @@ export const pack = async (
// Poll // Poll
if (meId && _note.poll && !hide) { if (meId && _note.poll && !hide) {
_note.poll = (async (poll) => { _note.poll = (async poll => {
const vote = await PollVote const vote = await PollVote
.findOne({ .findOne({
userId: meId, userId: meId,
@@ -15,6 +15,11 @@ const log = debug('misskey:activitypub');
export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise<void> { export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise<void> {
const uri = activity.id || activity; const uri = activity.id || activity;
// アナウンサーが凍結されていたらスキップ
if (actor.isSuspended) {
return;
}
if (typeof uri !== 'string') { if (typeof uri !== 'string') {
throw new Error('invalid announce'); throw new Error('invalid announce');
} }
+1 -1
View File
@@ -1,6 +1,6 @@
import config from '../../../config'; import config from '../../../config';
export default tag => ({ export default (tag: string) => ({
type: 'Hashtag', type: 'Hashtag',
href: `${config.url}/tags/${encodeURIComponent(tag)}`, href: `${config.url}/tags/${encodeURIComponent(tag)}`,
name: '#' + tag name: '#' + tag
@@ -0,0 +1,9 @@
export default (mention: {
uri: string;
username: string;
host: string;
}) => ({
type: 'Mention',
href: mention.uri,
name: `@${mention.username}@${mention.host}`
});
+15 -2
View File
@@ -1,5 +1,6 @@
import renderDocument from './document'; import renderDocument from './document';
import renderHashtag from './hashtag'; import renderHashtag from './hashtag';
import renderMention from './mention';
import config from '../../../config'; import config from '../../../config';
import DriveFile from '../../../models/drive-file'; import DriveFile from '../../../models/drive-file';
import Note, { INote } from '../../../models/note'; import Note, { INote } from '../../../models/note';
@@ -45,6 +46,18 @@ export default async function renderNote(note: INote, dive = true) {
const attributedTo = `${config.url}/users/${user._id}`; const attributedTo = `${config.url}/users/${user._id}`;
const mentions = note.mentionedRemoteUsers && note.mentionedRemoteUsers.length > 0
? note.mentionedRemoteUsers.map(x => x.uri)
: [];
const cc = ['public', 'home', 'followers'].includes(note.visibility)
? [`${attributedTo}/followers`].concat(mentions)
: [];
const hashtagTags = (note.tags || []).map(renderHashtag);
const mentionTags = (note.mentionedRemoteUsers || []).map(renderMention);
const tag = hashtagTags.concat(mentionTags)
return { return {
id: `${config.url}/notes/${note._id}`, id: `${config.url}/notes/${note._id}`,
type: 'Note', type: 'Note',
@@ -52,9 +65,9 @@ export default async function renderNote(note: INote, dive = true) {
content: toHtml(note), content: toHtml(note),
published: note.createdAt.toISOString(), published: note.createdAt.toISOString(),
to: 'https://www.w3.org/ns/activitystreams#Public', to: 'https://www.w3.org/ns/activitystreams#Public',
cc: `${attributedTo}/followers`, cc,
inReplyTo, inReplyTo,
attachment: (await promisedFiles).map(renderDocument), attachment: (await promisedFiles).map(renderDocument),
tag: (note.tags || []).map(renderHashtag) tag
}; };
} }
+9 -1
View File
@@ -4,7 +4,7 @@ import * as debug from 'debug';
import User, { IUser } from '../../../models/user'; import User, { IUser } from '../../../models/user';
import Mute from '../../../models/mute'; import Mute from '../../../models/mute';
import { pack as packNote } from '../../../models/note'; import { pack as packNote, pack } from '../../../models/note';
import readNotification from '../common/read-notification'; import readNotification from '../common/read-notification';
import call from '../call'; import call from '../call';
import { IApp } from '../../../models/app'; import { IApp } from '../../../models/app';
@@ -48,6 +48,14 @@ export default async function(
} }
//#endregion //#endregion
// Renoteなら再pack
if (x.type == 'note' && x.body.renoteId != null) {
x.body.renote = await pack(x.body.renoteId, user, {
detail: true
});
data = JSON.stringify(x);
}
connection.send(data); connection.send(data);
} catch (e) { } catch (e) {
connection.send(data); connection.send(data);
+8
View File
@@ -3,6 +3,7 @@ import * as redis from 'redis';
import { IUser } from '../../../models/user'; import { IUser } from '../../../models/user';
import Mute from '../../../models/mute'; import Mute from '../../../models/mute';
import { pack } from '../../../models/note';
export default async function( export default async function(
request: websocket.request, request: websocket.request,
@@ -31,6 +32,13 @@ export default async function(
} }
//#endregion //#endregion
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await pack(note.renoteId, user, {
detail: true
});
}
connection.send(JSON.stringify({ connection.send(JSON.stringify({
type: 'note', type: 'note',
body: note body: note
+56 -49
View File
@@ -204,6 +204,62 @@ export default async (user: IUser, data: {
return packAp(content); return packAp(content);
}; };
//#region メンション
if (data.text) {
// TODO: Drop dupulicates
const mentionTokens = tokens
.filter(t => t.type == 'mention');
// TODO: Drop dupulicates
const mentionedUsers = (await Promise.all(mentionTokens.map(async m => {
try {
return await resolveUser(m.username, m.host);
} catch (e) {
return null;
}
}))).filter(x => x != null);
// Append mentions data
if (mentionedUsers.length > 0) {
const set = {
mentions: mentionedUsers.map(u => u._id),
mentionedRemoteUsers: mentionedUsers.filter(u => isRemoteUser(u)).map(u => ({
uri: (u as IRemoteUser).uri,
username: u.username,
host: u.host
}))
};
Note.update({ _id: note._id }, {
$set: set
});
Object.assign(note, set);
}
mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => {
event(u, 'mention', noteObj);
// 既に言及されたユーザーに対する返信や引用renoteの場合も無視
if (data.reply && data.reply.userId.equals(u._id)) return;
if (data.renote && data.renote.userId.equals(u._id)) return;
// Create notification
notify(u._id, user._id, 'mention', {
noteId: note._id
});
nm.push(u._id, 'mention');
});
if (isLocalUser(user)) {
mentionedUsers.filter(u => isRemoteUser(u)).forEach(async u => {
deliver(user, await render(), (u as IRemoteUser).inbox);
});
}
}
//#endregion
if (!silent) { if (!silent) {
if (isLocalUser(user)) { if (isLocalUser(user)) {
if (note.visibility == 'private' || note.visibility == 'followers' || note.visibility == 'specified') { if (note.visibility == 'private' || note.visibility == 'followers' || note.visibility == 'specified') {
@@ -287,55 +343,6 @@ export default async (user: IUser, data: {
} }
//#endergion //#endergion
//#region メンション
if (data.text) {
// TODO: Drop dupulicates
const mentions = tokens
.filter(t => t.type == 'mention');
let mentionedUsers = await Promise.all(mentions.map(async m => {
try {
return await resolveUser(m.username, m.host);
} catch (e) {
return null;
}
}));
// TODO: Drop dupulicates
mentionedUsers = mentionedUsers.filter(x => x != null);
mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => {
event(u, 'mention', noteObj);
// 既に言及されたユーザーに対する返信や引用renoteの場合も無視
if (data.reply && data.reply.userId.equals(u._id)) return;
if (data.renote && data.renote.userId.equals(u._id)) return;
// Create notification
notify(u._id, user._id, 'mention', {
noteId: note._id
});
nm.push(u._id, 'mention');
});
if (isLocalUser(user)) {
mentionedUsers.filter(u => isRemoteUser(u)).forEach(async u => {
deliver(user, await render(), (u as IRemoteUser).inbox);
});
}
// Append mentions data
if (mentionedUsers.length > 0) {
Note.update({ _id: note._id }, {
$set: {
mentions: mentionedUsers.map(u => u._id)
}
});
}
}
//#endregion
// If has in reply to note // If has in reply to note
if (data.reply) { if (data.reply) {
// Increment replies count // Increment replies count
+1
View File
@@ -20,6 +20,7 @@ export default async function(user: IUser, note: INote) {
$set: { $set: {
deletedAt: new Date(), deletedAt: new Date(),
text: null, text: null,
tags: [],
mediaIds: [], mediaIds: [],
poll: null poll: null
} }