Compare commits

...

11 Commits

Author SHA1 Message Date
dc80d5d376 10.59.4 2018-12-02 07:08:26 +09:00
d3544f9637 Update README.md [AUTOGEN] (#3466) 2018-12-02 07:05:22 +09:00
864b6ad1bd Resolve #1826 2018-12-02 07:02:08 +09:00
c58027e521 [MFM] Better hashtag detection 2018-12-02 06:53:57 +09:00
10fb399588 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-12-02 06:44:25 +09:00
10f466c895 Improve performance 2018-12-02 06:44:18 +09:00
32068b4bcc Update README.md [AUTOGEN] (#3465) 2018-12-02 06:16:24 +09:00
8bc5febe66 [Client] Add missing icon (#3464) 2018-12-02 03:43:05 +09:00
20335e23f9 Resolve external recommended users (#3462)
* Resolve external recommended users

* Skip unresolvable users

* Fix indent

* Use original for unresolvable users
2018-12-02 03:42:45 +09:00
fe707f88a4 [MFM] Better MFM parsing 2018-12-01 10:40:09 +09:00
9b23ebd4a3 🎨 2018-12-01 09:45:48 +09:00
19 changed files with 96 additions and 77 deletions

View File

@ -116,7 +116,7 @@ Please see [Contribution guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td> <td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table> </tr></table>
**Last updated:** Tue, 27 Nov 2018 06:24:05 UTC **Last updated:** Sat, 01 Dec 2018 22:05:05 UTC
<!-- PATREON_END --> <!-- PATREON_END -->
:four_leaf_clover: Copyright :four_leaf_clover: Copyright

View File

@ -1,8 +1,8 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "10.59.3", "version": "10.59.4",
"clientVersion": "2.0.12313", "clientVersion": "2.0.12323",
"codename": "nighthike", "codename": "nighthike",
"main": "./built/index.js", "main": "./built/index.js",
"private": true, "private": true,

View File

@ -187,7 +187,8 @@ export default Vue.extend({
} else { } else {
this.$root.api('users/search', { this.$root.api('users/search', {
query: this.q, query: this.q,
limit: 10 limit: 10,
detail: false
}).then(users => { }).then(users => {
this.users = users; this.users = users;
this.fetching = false; this.fetching = false;

View File

@ -116,7 +116,8 @@ export default Vue.extend({
this.$root.api('users/search', { this.$root.api('users/search', {
query: this.q, query: this.q,
localOnly: true, localOnly: true,
limit: 10 limit: 10,
detail: false
}).then(users => { }).then(users => {
this.result = users.filter(user => user.id != this.$store.state.i.id); this.result = users.filter(user => user.id != this.$store.state.i.id);
}); });

View File

@ -39,7 +39,7 @@
</header> </header>
<div class="body"> <div class="body">
<p v-if="appearNote.cw != null" class="cw"> <p v-if="appearNote.cw != null" class="cw">
<span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span> <misskey-flavored-markdown v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" />
<mk-cw-button v-model="showContent"/> <mk-cw-button v-model="showContent"/>
</p> </p>
<div class="content" v-show="appearNote.cw == null || showContent"> <div class="content" v-show="appearNote.cw == null || showContent">

View File

@ -5,7 +5,7 @@
<mk-note-header class="header" :note="note" :mini="true"/> <mk-note-header class="header" :note="note" :mini="true"/>
<div class="body"> <div class="body">
<p v-if="note.cw != null" class="cw"> <p v-if="note.cw != null" class="cw">
<span class="text" v-if="note.cw != ''">{{ note.cw }}</span> <misskey-flavored-markdown v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" />
<mk-cw-button v-model="showContent"/> <mk-cw-button v-model="showContent"/>
</p> </p>
<div class="content" v-show="note.cw == null || showContent"> <div class="content" v-show="note.cw == null || showContent">

View File

@ -5,7 +5,7 @@
<mk-note-header class="header" :note="note"/> <mk-note-header class="header" :note="note"/>
<div class="body"> <div class="body">
<p v-if="note.cw != null" class="cw"> <p v-if="note.cw != null" class="cw">
<span class="text" v-if="note.cw != ''">{{ note.cw }}</span> <misskey-flavored-markdown v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" />
<mk-cw-button v-model="showContent"/> <mk-cw-button v-model="showContent"/>
</p> </p>
<div class="content" v-show="note.cw == null || showContent"> <div class="content" v-show="note.cw == null || showContent">

View File

@ -20,7 +20,7 @@
<mk-note-header class="header" :note="appearNote" :mini="mini"/> <mk-note-header class="header" :note="appearNote" :mini="mini"/>
<div class="body" v-if="appearNote.deletedAt == null"> <div class="body" v-if="appearNote.deletedAt == null">
<p v-if="appearNote.cw != null" class="cw"> <p v-if="appearNote.cw != null" class="cw">
<span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span> <misskey-flavored-markdown v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" />
<mk-cw-button v-model="showContent"/> <mk-cw-button v-model="showContent"/>
</p> </p>
<div class="content" v-show="appearNote.cw == null || showContent"> <div class="content" v-show="appearNote.cw == null || showContent">

View File

@ -265,6 +265,7 @@ export default Vue.extend({
display inline-block display inline-block
overflow hidden overflow hidden
max-height 48px max-height 48px
word-break break-all
.note-ref .note-ref
color var(--noteText) color var(--noteText)

View File

@ -144,6 +144,8 @@ import {
faHdd as farHdd, faHdd as farHdd,
faMoon as farMoon, faMoon as farMoon,
faPlayCircle as farPlayCircle, faPlayCircle as farPlayCircle,
faLightbulb as farLightbulb,
faStickyNote as farStickyNote,
} from '@fortawesome/free-regular-svg-icons'; } from '@fortawesome/free-regular-svg-icons';
import { import {
@ -270,6 +272,8 @@ library.add(
farHdd, farHdd,
farMoon, farMoon,
farPlayCircle, farPlayCircle,
farLightbulb,
farStickyNote,
fabTwitter, fabTwitter,
fabGithub, fabGithub,

View File

@ -26,7 +26,7 @@
</header> </header>
<div class="body"> <div class="body">
<p v-if="appearNote.cw != null" class="cw"> <p v-if="appearNote.cw != null" class="cw">
<span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span> <misskey-flavored-markdown v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" />
<mk-cw-button v-model="showContent"/> <mk-cw-button v-model="showContent"/>
</p> </p>
<div class="content" v-show="appearNote.cw == null || showContent"> <div class="content" v-show="appearNote.cw == null || showContent">

View File

@ -5,7 +5,7 @@
<mk-note-header class="header" :note="note" :mini="true"/> <mk-note-header class="header" :note="note" :mini="true"/>
<div class="body"> <div class="body">
<p v-if="note.cw != null" class="cw"> <p v-if="note.cw != null" class="cw">
<span class="text" v-if="note.cw != ''">{{ note.cw }}</span> <misskey-flavored-markdown v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis" />
<mk-cw-button v-model="showContent"/> <mk-cw-button v-model="showContent"/>
</p> </p>
<div class="content" v-show="note.cw == null || showContent"> <div class="content" v-show="note.cw == null || showContent">

View File

@ -16,7 +16,7 @@
<mk-note-header class="header" :note="appearNote" :mini="true"/> <mk-note-header class="header" :note="appearNote" :mini="true"/>
<div class="body" v-if="appearNote.deletedAt == null"> <div class="body" v-if="appearNote.deletedAt == null">
<p v-if="appearNote.cw != null" class="cw"> <p v-if="appearNote.cw != null" class="cw">
<span class="text" v-if="appearNote.cw != ''">{{ appearNote.cw }}</span> <misskey-flavored-markdown v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" />
<mk-cw-button v-model="showContent"/> <mk-cw-button v-model="showContent"/>
</p> </p>
<div class="content" v-show="appearNote.cw == null || showContent"> <div class="content" v-show="appearNote.cw == null || showContent">

View File

@ -26,45 +26,6 @@ export default (source: string): Node[] => {
nodes = concatText(nodes); nodes = concatText(nodes);
concatTextRecursive(nodes); concatTextRecursive(nodes);
function getBeforeTextNode(node: Node): Node {
if (node == null) return null;
if (node.name == 'text') return node;
if (node.children) return getBeforeTextNode(node.children[node.children.length - 1]);
return null;
}
function getAfterTextNode(node: Node): Node {
if (node == null) return null;
if (node.name == 'text') return node;
if (node.children) return getBeforeTextNode(node.children[0]);
return null;
}
function isBlockNode(node: Node): boolean {
return ['blockCode', 'center', 'quote', 'title'].includes(node.name);
}
/**
* ブロック要素の前後にある改行を削除します
* (ブロック要素自体が改行の役割を果たすため、余計に改行されてしまう)
* @param nodes
*/
const removeNeedlessLineBreaks = (nodes: Node[]) => {
nodes.forEach((node, i) => {
if (node.children) removeNeedlessLineBreaks(node.children);
if (isBlockNode(node)) {
const before = getBeforeTextNode(nodes[i - 1]);
const after = getAfterTextNode(nodes[i + 1]);
if (before && before.props.text.endsWith('\n')) {
before.props.text = before.props.text.substring(0, before.props.text.length - 1);
}
if (after && after.props.text.startsWith('\n')) {
after.props.text = after.props.text.substring(1);
}
}
});
};
const removeEmptyTextNodes = (nodes: Node[]) => { const removeEmptyTextNodes = (nodes: Node[]) => {
nodes.forEach(n => { nodes.forEach(n => {
if (n.children) { if (n.children) {
@ -74,8 +35,6 @@ export default (source: string): Node[] => {
return nodes.filter(n => !(n.name == 'text' && n.props.text == '')); return nodes.filter(n => !(n.name == 'text' && n.props.text == ''));
}; };
removeNeedlessLineBreaks(nodes);
nodes = removeEmptyTextNodes(nodes); nodes = removeEmptyTextNodes(nodes);
return nodes; return nodes;

View File

@ -162,7 +162,7 @@ const mfm = P.createLanguage({
let hashtag = match[1]; let hashtag = match[1];
hashtag = hashtag.substr(0, getTrailingPosition(hashtag)); hashtag = hashtag.substr(0, getTrailingPosition(hashtag));
if (hashtag.match(/^[0-9]+$/)) return P.makeFailure(i, 'not a hashtag'); if (hashtag.match(/^[0-9]+$/)) return P.makeFailure(i, 'not a hashtag');
if (!['\n', ' ', ' ', '(', '「', null, undefined].includes(input[i - 1])) return P.makeFailure(i, 'require space before "#"'); if (input[i - 1] != null && input[i - 1].match(/[a-z0-9]/i)) return P.makeFailure(i, 'not a hashtag');
return P.makeSuccess(i + ('#' + hashtag).length, makeNode('hashtag', { hashtag: hashtag })); return P.makeSuccess(i + ('#' + hashtag).length, makeNode('hashtag', { hashtag: hashtag }));
}), }),
//#endregion //#endregion
@ -254,7 +254,7 @@ const mfm = P.createLanguage({
const qInner = quote.join('\n').replace(/^>/gm, '').replace(/^ /gm, ''); const qInner = quote.join('\n').replace(/^>/gm, '').replace(/^ /gm, '');
if (qInner == '') return P.makeFailure(i, 'not a quote'); if (qInner == '') return P.makeFailure(i, 'not a quote');
const contents = r.root.tryParse(qInner); const contents = r.root.tryParse(qInner);
return P.makeSuccess(i + quote.join('\n').length, makeNodeWithChildren('quote', contents)); return P.makeSuccess(i + quote.join('\n').length + 1, makeNodeWithChildren('quote', contents));
})), })),
//#endregion //#endregion

View File

@ -1,12 +1,13 @@
const ms = require('ms'); const ms = require('ms');
import $ from 'cafy'; import $ from 'cafy';
import User, { pack } from '../../../../models/user'; import User, { pack, ILocalUser } from '../../../../models/user';
import { getFriendIds } from '../../common/get-friends'; import { getFriendIds } from '../../common/get-friends';
import Mute from '../../../../models/mute'; import Mute from '../../../../models/mute';
import * as request from 'request'; import * as request from 'request-promise-native';
import config from '../../../../config'; import config from '../../../../config';
import define from '../../define'; import define from '../../define';
import fetchMeta from '../../../../misc/fetch-meta'; import fetchMeta from '../../../../misc/fetch-meta';
import resolveUser from '../../../../remote/resolve-user';
export const meta = { export const meta = {
desc: { desc: {
@ -53,13 +54,10 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
json: true, json: true,
followRedirect: true, followRedirect: true,
followAllRedirects: true followAllRedirects: true
}, (error: any, response: any, body: any) => { })
if (!error && response.statusCode == 200) { .then(body => convertUsers(body, me))
res(body); .then(packed => res(packed))
} else { .catch(e => rej(e));
res([]);
}
});
} else { } else {
// ID list of the user itself and other users who the user follows // ID list of the user itself and other users who the user follows
const followingIds = await getFriendIds(me._id); const followingIds = await getFriendIds(me._id);
@ -90,3 +88,30 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
res(await Promise.all(users.map(user => pack(user, me, { detail: true })))); res(await Promise.all(users.map(user => pack(user, me, { detail: true }))));
} }
})); }));
type IRecommendUser = {
name: string;
username: string;
host: string;
description: string;
avatarUrl: string;
};
/**
* Resolve/Pack dummy users
*/
async function convertUsers(src: IRecommendUser[], me: ILocalUser) {
const packed = await Promise.all(src.map(async x => {
const user = await resolveUser(x.username, x.host)
.catch(() => {
console.warn(`Can't resolve ${x.username}@${x.host}`);
return null;
});
if (user == null) return x;
return await pack(user, me, { detail: true });
}));
return packed;
}

View File

@ -41,6 +41,14 @@ export const meta = {
'ja-JP': 'ローカルユーザーのみ検索対象にするか否か' 'ja-JP': 'ローカルユーザーのみ検索対象にするか否か'
} }
}, },
detail: {
validator: $.bool.optional,
default: true,
desc: {
'ja-JP': '詳細なユーザー情報を含めるか否か'
}
},
}, },
}; };
@ -72,6 +80,5 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
} }
} }
// Serialize res(await Promise.all(users.map(user => pack(user, me, { detail: ps.detail }))));
res(await Promise.all(users.map(user => pack(user, me, { detail: true }))));
})); }));

View File

@ -155,12 +155,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
// Parse MFM // Parse MFM
const tokens = data.text ? parse(data.text) : []; const tokens = data.text ? parse(data.text) : [];
const cwTokens = data.cw ? parse(data.cw) : [];
const combinedTokens = tokens.concat(cwTokens);
const tags = extractHashtags(tokens); const tags = extractHashtags(combinedTokens);
const emojis = extractEmojis(tokens); const emojis = extractEmojis(combinedTokens);
const mentionedUsers = data.apMentions || await extractMentionedUsers(user, tokens); const mentionedUsers = data.apMentions || await extractMentionedUsers(user, combinedTokens);
if (data.reply && !user._id.equals(data.reply.userId) && !mentionedUsers.some(u => u._id.equals(data.reply.userId))) { if (data.reply && !user._id.equals(data.reply.userId) && !mentionedUsers.some(u => u._id.equals(data.reply.userId))) {
mentionedUsers.push(await User.findOne({ _id: data.reply.userId })); mentionedUsers.push(await User.findOne({ _id: data.reply.userId }));

View File

@ -187,9 +187,9 @@ describe('Text', () => {
}); });
it('with text (zenkaku)', () => { it('with text (zenkaku)', () => {
const tokens = analyze('こんにちは #世界'); const tokens = analyze('こんにちは#世界');
assert.deepEqual([ assert.deepEqual([
text('こんにちは '), text('こんにちは'),
node('hashtag', { hashtag: '世界' }) node('hashtag', { hashtag: '世界' })
], tokens); ], tokens);
}); });
@ -299,6 +299,7 @@ describe('Text', () => {
nodeWithChildren('quote', [ nodeWithChildren('quote', [
text('foo') text('foo')
]), ]),
text('\n'),
nodeWithChildren('quote', [ nodeWithChildren('quote', [
text('bar') text('bar')
]), ]),
@ -358,7 +359,7 @@ describe('Text', () => {
it('with before and after texts', () => { it('with before and after texts', () => {
const tokens = analyze('before\n> foo\nafter'); const tokens = analyze('before\n> foo\nafter');
assert.deepEqual([ assert.deepEqual([
text('before'), text('before\n'),
nodeWithChildren('quote', [ nodeWithChildren('quote', [
text('foo') text('foo')
]), ]),
@ -366,6 +367,24 @@ describe('Text', () => {
], tokens); ], tokens);
}); });
it('multiple quotes', () => {
const tokens = analyze('> foo\nbar\n\n> foo\nbar\n\n> foo\nbar');
assert.deepEqual([
nodeWithChildren('quote', [
text('foo')
]),
text('bar\n\n'),
nodeWithChildren('quote', [
text('foo')
]),
text('bar\n\n'),
nodeWithChildren('quote', [
text('foo')
]),
text('bar'),
], tokens);
});
it('require line break before ">"', () => { it('require line break before ">"', () => {
const tokens = analyze('foo>bar'); const tokens = analyze('foo>bar');
assert.deepEqual([ assert.deepEqual([
@ -388,11 +407,11 @@ describe('Text', () => {
it('trim line breaks', () => { it('trim line breaks', () => {
const tokens = analyze('foo\n\n>a\n>>b\n>>\n>>>\n>>>c\n>>>\n>d\n\n'); const tokens = analyze('foo\n\n>a\n>>b\n>>\n>>>\n>>>c\n>>>\n>d\n\n');
assert.deepEqual([ assert.deepEqual([
text('foo\n'), text('foo\n\n'),
nodeWithChildren('quote', [ nodeWithChildren('quote', [
text('a'), text('a\n'),
nodeWithChildren('quote', [ nodeWithChildren('quote', [
text('b\n'), text('b\n\n'),
nodeWithChildren('quote', [ nodeWithChildren('quote', [
text('\nc\n') text('\nc\n')
]) ])
@ -664,7 +683,7 @@ describe('Text', () => {
it('with before and after texts', () => { it('with before and after texts', () => {
const tokens = analyze('before\n【foo】\nafter'); const tokens = analyze('before\n【foo】\nafter');
assert.deepEqual([ assert.deepEqual([
text('before'), text('before\n'),
nodeWithChildren('title', [ nodeWithChildren('title', [
text('foo') text('foo')
]), ]),