リモートのカスタム絵文字リアクションを表示できるように (#6239)
* リモートのカスタム絵文字リアクションを表示できるように * AP * DBマイグレーション * ローカルのリアクションの. * fix * fix * fix * space
This commit is contained in:
parent
cda1803e59
commit
9b07c5af05
12 changed files with 185 additions and 41 deletions
12
migration/1586641139527-remote-reaction.ts
Normal file
12
migration/1586641139527-remote-reaction.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||||
|
|
||||||
|
export class remoteReaction1586641139527 implements MigrationInterface {
|
||||||
|
name = 'remoteReaction1586641139527'
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(260)`, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(130)`, undefined);
|
||||||
|
}
|
||||||
|
}
|
|
@ -301,6 +301,14 @@ export default Vue.extend({
|
||||||
case 'reacted': {
|
case 'reacted': {
|
||||||
const reaction = body.reaction;
|
const reaction = body.reaction;
|
||||||
|
|
||||||
|
if (body.emoji) {
|
||||||
|
const emojis = this.appearNote.emojis || [];
|
||||||
|
if (!emojis.includes(body.emoji)) {
|
||||||
|
emojis.push(body.emoji);
|
||||||
|
Vue.set(this.appearNote, 'emojis', emojis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.appearNote.reactions == null) {
|
if (this.appearNote.reactions == null) {
|
||||||
Vue.set(this.appearNote, 'reactions', {});
|
Vue.set(this.appearNote, 'reactions', {});
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<fa :icon="faReply" v-else-if="notification.type === 'reply'"/>
|
<fa :icon="faReply" v-else-if="notification.type === 'reply'"/>
|
||||||
<fa :icon="faAt" v-else-if="notification.type === 'mention'"/>
|
<fa :icon="faAt" v-else-if="notification.type === 'mention'"/>
|
||||||
<fa :icon="faQuoteLeft" v-else-if="notification.type === 'quote'"/>
|
<fa :icon="faQuoteLeft" v-else-if="notification.type === 'quote'"/>
|
||||||
<x-reaction-icon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :no-style="true"/>
|
<x-reaction-icon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :customEmojis="notification.note.emojis" :no-style="true"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tail">
|
<div class="tail">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<mk-emoji :emoji="reaction.startsWith(':') ? null : reaction" :name="reaction.startsWith(':') ? reaction.substr(1, reaction.length - 2) : null" :is-reaction="true" :normal="true" :no-style="noStyle"/>
|
<mk-emoji :emoji="reaction.startsWith(':') ? null : reaction" :name="reaction.startsWith(':') ? reaction.substr(1, reaction.length - 2) : null" :customEmojis="customEmojis" :is-reaction="true" :normal="true" :no-style="noStyle"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -12,6 +12,10 @@ export default Vue.extend({
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
customEmojis: {
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
noStyle: {
|
noStyle: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
ref="reaction"
|
ref="reaction"
|
||||||
v-particle
|
v-particle
|
||||||
>
|
>
|
||||||
<x-reaction-icon :reaction="reaction" ref="icon"/>
|
<x-reaction-icon :reaction="reaction" :customEmojis="note.emojis" ref="icon"/>
|
||||||
<span>{{ count }}</span>
|
<span>{{ count }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { emojiRegex } from './emoji-regex';
|
import { emojiRegex } from './emoji-regex';
|
||||||
import { fetchMeta } from './fetch-meta';
|
import { fetchMeta } from './fetch-meta';
|
||||||
import { Emojis } from '../models';
|
import { Emojis } from '../models';
|
||||||
|
import { toPunyNullable } from './convert-host';
|
||||||
|
|
||||||
const legacies: Record<string, string> = {
|
const legacies: Record<string, string> = {
|
||||||
'like': '👍',
|
'like': '👍',
|
||||||
|
@ -40,12 +41,20 @@ export function convertLegacyReactions(reactions: Record<string, number>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return _reactions;
|
const _reactions2 = {} as Record<string, number>;
|
||||||
|
|
||||||
|
for (const reaction of Object.keys(_reactions)) {
|
||||||
|
_reactions2[decodeReaction(reaction).reaction] = _reactions[reaction];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toDbReaction(reaction?: string | null): Promise<string> {
|
return _reactions2;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> {
|
||||||
if (reaction == null) return await getFallbackReaction();
|
if (reaction == null) return await getFallbackReaction();
|
||||||
|
|
||||||
|
reacterHost = toPunyNullable(reacterHost);
|
||||||
|
|
||||||
// 文字列タイプのリアクションを絵文字に変換
|
// 文字列タイプのリアクションを絵文字に変換
|
||||||
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
||||||
|
|
||||||
|
@ -61,18 +70,58 @@ export async function toDbReaction(reaction?: string | null): Promise<string> {
|
||||||
|
|
||||||
const custom = reaction.match(/^:([\w+-]+):$/);
|
const custom = reaction.match(/^:([\w+-]+):$/);
|
||||||
if (custom) {
|
if (custom) {
|
||||||
|
const name = custom[1];
|
||||||
const emoji = await Emojis.findOne({
|
const emoji = await Emojis.findOne({
|
||||||
host: null,
|
host: reacterHost || null,
|
||||||
name: custom[1],
|
name,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (emoji) return reaction;
|
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`
|
||||||
}
|
}
|
||||||
|
|
||||||
return await getFallbackReaction();
|
return await getFallbackReaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DecodedReaction = {
|
||||||
|
/**
|
||||||
|
* リアクション名 (Unicode Emoji or ':name@hostname' or ':name@.')
|
||||||
|
*/
|
||||||
|
reaction: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* name (カスタム絵文字の場合name, Emojiクエリに使う)
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* host (カスタム絵文字の場合host, Emojiクエリに使う)
|
||||||
|
*/
|
||||||
|
host?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function decodeReaction(str: string): DecodedReaction {
|
||||||
|
const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/);
|
||||||
|
|
||||||
|
if (custom) {
|
||||||
|
const name = custom[1];
|
||||||
|
const host = custom[2] || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
reaction: `:${name}@${host || '.'}:`, // ローカル分は@以降を省略するのではなく.にする
|
||||||
|
name,
|
||||||
|
host
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
reaction: str,
|
||||||
|
name: undefined,
|
||||||
|
host: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function convertLegacyReaction(reaction: string): string {
|
export function convertLegacyReaction(reaction: string): string {
|
||||||
|
reaction = decodeReaction(reaction).reaction;
|
||||||
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
||||||
return reaction;
|
return reaction;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ export class NoteReaction {
|
||||||
public note: Note | null;
|
public note: Note | null;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 130
|
length: 260
|
||||||
})
|
})
|
||||||
public reaction: string;
|
public reaction: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,11 @@ import { Emojis, Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls
|
||||||
import { ensure } from '../../prelude/ensure';
|
import { ensure } from '../../prelude/ensure';
|
||||||
import { SchemaType } from '../../misc/schema';
|
import { SchemaType } from '../../misc/schema';
|
||||||
import { awaitAll } from '../../prelude/await-all';
|
import { awaitAll } from '../../prelude/await-all';
|
||||||
import { convertLegacyReaction, convertLegacyReactions } from '../../misc/reaction-lib';
|
import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '../../misc/reaction-lib';
|
||||||
import { toString } from '../../mfm/toString';
|
import { toString } from '../../mfm/toString';
|
||||||
import { parse } from '../../mfm/parse';
|
import { parse } from '../../mfm/parse';
|
||||||
|
import { Emoji } from '../entities/emoji';
|
||||||
|
import { concat } from '../../prelude/array';
|
||||||
|
|
||||||
export type PackedNote = SchemaType<typeof packedNoteSchema>;
|
export type PackedNote = SchemaType<typeof packedNoteSchema>;
|
||||||
|
|
||||||
|
@ -129,31 +131,61 @@ export class NoteRepository extends Repository<Note> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添付用emojisを解決する
|
||||||
|
* @param emojiNames Note等に添付されたカスタム絵文字名 (:は含めない)
|
||||||
|
* @param noteUserHost Noteのホスト
|
||||||
|
* @param reactionNames Note等にリアクションされたカスタム絵文字名 (:は含めない)
|
||||||
|
*/
|
||||||
async function populateEmojis(emojiNames: string[], noteUserHost: string | null, reactionNames: string[]) {
|
async function populateEmojis(emojiNames: string[], noteUserHost: string | null, reactionNames: string[]) {
|
||||||
const where = [] as {}[];
|
let all = [] as {
|
||||||
|
name: string,
|
||||||
|
url: string
|
||||||
|
}[];
|
||||||
|
|
||||||
|
// カスタム絵文字
|
||||||
if (emojiNames?.length > 0) {
|
if (emojiNames?.length > 0) {
|
||||||
where.push({
|
const tmp = await Emojis.find({
|
||||||
|
where: {
|
||||||
name: In(emojiNames),
|
name: In(emojiNames),
|
||||||
host: noteUserHost
|
host: noteUserHost
|
||||||
});
|
},
|
||||||
|
select: ['name', 'host', 'url']
|
||||||
|
}).then(emojis => emojis.map((emoji: Emoji) => {
|
||||||
|
return {
|
||||||
|
name: emoji.name,
|
||||||
|
url: emoji.url,
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
all = concat([all, tmp]);
|
||||||
}
|
}
|
||||||
|
|
||||||
reactionNames = reactionNames?.filter(x => x.match(/^:[^:]+:$/)).map(x => x.replace(/:/g, ''));
|
const customReactions = reactionNames?.map(x => decodeReaction(x)).filter(x => x.name);
|
||||||
|
|
||||||
if (reactionNames?.length > 0) {
|
if (customReactions?.length > 0) {
|
||||||
|
const where = [] as {}[];
|
||||||
|
|
||||||
|
for (const customReaction of customReactions) {
|
||||||
where.push({
|
where.push({
|
||||||
name: In(reactionNames),
|
name: customReaction.name,
|
||||||
host: null
|
host: customReaction.host
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (where.length === 0) return [];
|
const tmp = await Emojis.find({
|
||||||
|
|
||||||
return Emojis.find({
|
|
||||||
where,
|
where,
|
||||||
select: ['name', 'host', 'url', 'aliases']
|
select: ['name', 'host', 'url']
|
||||||
});
|
}).then(emojis => emojis.map((emoji: Emoji) => {
|
||||||
|
return {
|
||||||
|
name: `${emoji.name}@${emoji.host || '.'}`, // @host付きでローカルは.
|
||||||
|
url: emoji.url,
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
all = concat([all, tmp]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return all;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function populateMyReaction() {
|
async function populateMyReaction() {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { IRemoteUser } from '../../../models/entities/user';
|
import { IRemoteUser } from '../../../models/entities/user';
|
||||||
import { ILike, getApId } from '../type';
|
import { ILike, getApId } from '../type';
|
||||||
import create from '../../../services/note/reaction/create';
|
import create from '../../../services/note/reaction/create';
|
||||||
import { fetchNote } from '../models/note';
|
import { fetchNote, extractEmojis } from '../models/note';
|
||||||
|
|
||||||
export default async (actor: IRemoteUser, activity: ILike) => {
|
export default async (actor: IRemoteUser, activity: ILike) => {
|
||||||
const targetUri = getApId(activity.object);
|
const targetUri = getApId(activity.object);
|
||||||
|
@ -11,6 +11,8 @@ export default async (actor: IRemoteUser, activity: ILike) => {
|
||||||
|
|
||||||
if (actor.id === note.userId) return `skip: cannot react to my note`;
|
if (actor.id === note.userId) return `skip: cannot react to my note`;
|
||||||
|
|
||||||
|
await extractEmojis(activity.tag || [], actor.host).catch(() => null);
|
||||||
|
|
||||||
await create(actor, note, activity._misskey_reaction || activity.content || activity.name);
|
await create(actor, note, activity._misskey_reaction || activity.content || activity.name);
|
||||||
return `ok`;
|
return `ok`;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,30 @@
|
||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
import { NoteReaction } from '../../../models/entities/note-reaction';
|
import { NoteReaction } from '../../../models/entities/note-reaction';
|
||||||
import { Note } from '../../../models/entities/note';
|
import { Note } from '../../../models/entities/note';
|
||||||
|
import { Emojis } from '../../../models';
|
||||||
|
import renderEmoji from './emoji';
|
||||||
|
|
||||||
export const renderLike = (noteReaction: NoteReaction, note: Note) => ({
|
export const renderLike = async (noteReaction: NoteReaction, note: Note) => {
|
||||||
|
const reaction = noteReaction.reaction;
|
||||||
|
|
||||||
|
const object = {
|
||||||
type: 'Like',
|
type: 'Like',
|
||||||
id: `${config.url}/likes/${noteReaction.id}`,
|
id: `${config.url}/likes/${noteReaction.id}`,
|
||||||
actor: `${config.url}/users/${noteReaction.userId}`,
|
actor: `${config.url}/users/${noteReaction.userId}`,
|
||||||
object: note.uri ? note.uri : `${config.url}/notes/${noteReaction.noteId}`,
|
object: note.uri ? note.uri : `${config.url}/notes/${noteReaction.noteId}`,
|
||||||
content: noteReaction.reaction,
|
content: reaction,
|
||||||
_misskey_reaction: noteReaction.reaction
|
_misskey_reaction: reaction
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
if (reaction.startsWith(':')) {
|
||||||
|
const name = reaction.replace(/:/g, '');
|
||||||
|
const emoji = await Emojis.findOne({
|
||||||
|
name,
|
||||||
|
host: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (emoji) object.tag = [ renderEmoji(emoji) ];
|
||||||
|
}
|
||||||
|
|
||||||
|
return object;
|
||||||
|
};
|
||||||
|
|
|
@ -4,10 +4,10 @@ import { renderLike } from '../../../remote/activitypub/renderer/like';
|
||||||
import DeliverManager from '../../../remote/activitypub/deliver-manager';
|
import DeliverManager from '../../../remote/activitypub/deliver-manager';
|
||||||
import { renderActivity } from '../../../remote/activitypub/renderer';
|
import { renderActivity } from '../../../remote/activitypub/renderer';
|
||||||
import { IdentifiableError } from '../../../misc/identifiable-error';
|
import { IdentifiableError } from '../../../misc/identifiable-error';
|
||||||
import { toDbReaction } from '../../../misc/reaction-lib';
|
import { toDbReaction, decodeReaction } from '../../../misc/reaction-lib';
|
||||||
import { User, IRemoteUser } from '../../../models/entities/user';
|
import { User, IRemoteUser } from '../../../models/entities/user';
|
||||||
import { Note } from '../../../models/entities/note';
|
import { Note } from '../../../models/entities/note';
|
||||||
import { NoteReactions, Users, NoteWatchings, Notes, UserProfiles } from '../../../models';
|
import { NoteReactions, Users, NoteWatchings, Notes, UserProfiles, Emojis } from '../../../models';
|
||||||
import { Not } from 'typeorm';
|
import { Not } from 'typeorm';
|
||||||
import { perUserReactionsChart } from '../../chart';
|
import { perUserReactionsChart } from '../../chart';
|
||||||
import { genId } from '../../../misc/gen-id';
|
import { genId } from '../../../misc/gen-id';
|
||||||
|
@ -20,7 +20,7 @@ export default async (user: User, note: Note, reaction?: string) => {
|
||||||
throw new IdentifiableError('2d8e7297-1873-4c00-8404-792c68d7bef0', 'cannot react to my note');
|
throw new IdentifiableError('2d8e7297-1873-4c00-8404-792c68d7bef0', 'cannot react to my note');
|
||||||
}
|
}
|
||||||
|
|
||||||
reaction = await toDbReaction(reaction);
|
reaction = await toDbReaction(reaction, user.host);
|
||||||
|
|
||||||
const exist = await NoteReactions.findOne({
|
const exist = await NoteReactions.findOne({
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
|
@ -59,8 +59,27 @@ export default async (user: User, note: Note, reaction?: string) => {
|
||||||
|
|
||||||
perUserReactionsChart.update(user, note);
|
perUserReactionsChart.update(user, note);
|
||||||
|
|
||||||
|
// カスタム絵文字リアクションだったら絵文字情報も送る
|
||||||
|
const decodedReaction = decodeReaction(reaction);
|
||||||
|
|
||||||
|
let emoji = await Emojis.findOne({
|
||||||
|
where: {
|
||||||
|
name: decodedReaction.name,
|
||||||
|
host: decodedReaction.host
|
||||||
|
},
|
||||||
|
select: ['name', 'host', 'url']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (emoji) {
|
||||||
|
emoji = {
|
||||||
|
name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}`,
|
||||||
|
url: emoji.url
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
publishNoteStream(note.id, 'reacted', {
|
publishNoteStream(note.id, 'reacted', {
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
|
emoji: emoji,
|
||||||
userId: user.id
|
userId: user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -96,7 +115,7 @@ export default async (user: User, note: Note, reaction?: string) => {
|
||||||
|
|
||||||
//#region 配信
|
//#region 配信
|
||||||
if (Users.isLocalUser(user) && !note.localOnly) {
|
if (Users.isLocalUser(user) && !note.localOnly) {
|
||||||
const content = renderActivity(renderLike(inserted, note));
|
const content = renderActivity(await renderLike(inserted, note));
|
||||||
const dm = new DeliverManager(user, content);
|
const dm = new DeliverManager(user, content);
|
||||||
if (note.userHost !== null) {
|
if (note.userHost !== null) {
|
||||||
const reactee = await Users.findOne(note.userId)
|
const reactee = await Users.findOne(note.userId)
|
||||||
|
|
|
@ -44,7 +44,7 @@ export default async (user: User, note: Note) => {
|
||||||
|
|
||||||
//#region 配信
|
//#region 配信
|
||||||
if (Users.isLocalUser(user) && !note.localOnly) {
|
if (Users.isLocalUser(user) && !note.localOnly) {
|
||||||
const content = renderActivity(renderUndo(renderLike(exist, note), user));
|
const content = renderActivity(renderUndo(await renderLike(exist, note), user));
|
||||||
const dm = new DeliverManager(user, content);
|
const dm = new DeliverManager(user, content);
|
||||||
if (note.userHost !== null) {
|
if (note.userHost !== null) {
|
||||||
const reactee = await Users.findOne(note.userId)
|
const reactee = await Users.findOne(note.userId)
|
||||||
|
|
Loading…
Reference in a new issue