リモートのカスタム絵文字リアクションを表示できるように (#6239)
* リモートのカスタム絵文字リアクションを表示できるように * AP * DBマイグレーション * ローカルのリアクションの. * fix * fix * fix * space
This commit is contained in:
parent
cda1803e59
commit
9b07c5af05
|
@ -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': {
|
||||
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) {
|
||||
Vue.set(this.appearNote, 'reactions', {});
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<fa :icon="faReply" v-else-if="notification.type === 'reply'"/>
|
||||
<fa :icon="faAt" v-else-if="notification.type === 'mention'"/>
|
||||
<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 class="tail">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -12,6 +12,10 @@ export default Vue.extend({
|
|||
type: String,
|
||||
required: true
|
||||
},
|
||||
customEmojis: {
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
noStyle: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
ref="reaction"
|
||||
v-particle
|
||||
>
|
||||
<x-reaction-icon :reaction="reaction" ref="icon"/>
|
||||
<x-reaction-icon :reaction="reaction" :customEmojis="note.emojis" ref="icon"/>
|
||||
<span>{{ count }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { emojiRegex } from './emoji-regex';
|
||||
import { fetchMeta } from './fetch-meta';
|
||||
import { Emojis } from '../models';
|
||||
import { toPunyNullable } from './convert-host';
|
||||
|
||||
const legacies: Record<string, string> = {
|
||||
'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];
|
||||
}
|
||||
|
||||
return _reactions2;
|
||||
}
|
||||
|
||||
export async function toDbReaction(reaction?: string | null): Promise<string> {
|
||||
export async function toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> {
|
||||
if (reaction == null) return await getFallbackReaction();
|
||||
|
||||
reacterHost = toPunyNullable(reacterHost);
|
||||
|
||||
// 文字列タイプのリアクションを絵文字に変換
|
||||
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+-]+):$/);
|
||||
if (custom) {
|
||||
const name = custom[1];
|
||||
const emoji = await Emojis.findOne({
|
||||
host: null,
|
||||
name: custom[1],
|
||||
host: reacterHost || null,
|
||||
name,
|
||||
});
|
||||
|
||||
if (emoji) return reaction;
|
||||
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`
|
||||
}
|
||||
|
||||
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 {
|
||||
reaction = decodeReaction(reaction).reaction;
|
||||
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
||||
return reaction;
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ export class NoteReaction {
|
|||
public note: Note | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 130
|
||||
length: 260
|
||||
})
|
||||
public reaction: string;
|
||||
}
|
||||
|
|
|
@ -5,9 +5,11 @@ import { Emojis, Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls
|
|||
import { ensure } from '../../prelude/ensure';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
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 { parse } from '../../mfm/parse';
|
||||
import { Emoji } from '../entities/emoji';
|
||||
import { concat } from '../../prelude/array';
|
||||
|
||||
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[]) {
|
||||
const where = [] as {}[];
|
||||
let all = [] as {
|
||||
name: string,
|
||||
url: string
|
||||
}[];
|
||||
|
||||
// カスタム絵文字
|
||||
if (emojiNames?.length > 0) {
|
||||
where.push({
|
||||
name: In(emojiNames),
|
||||
host: noteUserHost
|
||||
});
|
||||
const tmp = await Emojis.find({
|
||||
where: {
|
||||
name: In(emojiNames),
|
||||
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) {
|
||||
where.push({
|
||||
name: In(reactionNames),
|
||||
host: null
|
||||
});
|
||||
if (customReactions?.length > 0) {
|
||||
const where = [] as {}[];
|
||||
|
||||
for (const customReaction of customReactions) {
|
||||
where.push({
|
||||
name: customReaction.name,
|
||||
host: customReaction.host
|
||||
});
|
||||
}
|
||||
|
||||
const tmp = await Emojis.find({
|
||||
where,
|
||||
select: ['name', 'host', 'url']
|
||||
}).then(emojis => emojis.map((emoji: Emoji) => {
|
||||
return {
|
||||
name: `${emoji.name}@${emoji.host || '.'}`, // @host付きでローカルは.
|
||||
url: emoji.url,
|
||||
};
|
||||
}));
|
||||
all = concat([all, tmp]);
|
||||
}
|
||||
|
||||
if (where.length === 0) return [];
|
||||
|
||||
return Emojis.find({
|
||||
where,
|
||||
select: ['name', 'host', 'url', 'aliases']
|
||||
});
|
||||
return all;
|
||||
}
|
||||
|
||||
async function populateMyReaction() {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { IRemoteUser } from '../../../models/entities/user';
|
||||
import { ILike, getApId } from '../type';
|
||||
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) => {
|
||||
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`;
|
||||
|
||||
await extractEmojis(activity.tag || [], actor.host).catch(() => null);
|
||||
|
||||
await create(actor, note, activity._misskey_reaction || activity.content || activity.name);
|
||||
return `ok`;
|
||||
};
|
||||
|
|
|
@ -1,12 +1,30 @@
|
|||
import config from '../../../config';
|
||||
import { NoteReaction } from '../../../models/entities/note-reaction';
|
||||
import { Note } from '../../../models/entities/note';
|
||||
import { Emojis } from '../../../models';
|
||||
import renderEmoji from './emoji';
|
||||
|
||||
export const renderLike = (noteReaction: NoteReaction, note: Note) => ({
|
||||
type: 'Like',
|
||||
id: `${config.url}/likes/${noteReaction.id}`,
|
||||
actor: `${config.url}/users/${noteReaction.userId}`,
|
||||
object: note.uri ? note.uri : `${config.url}/notes/${noteReaction.noteId}`,
|
||||
content: noteReaction.reaction,
|
||||
_misskey_reaction: noteReaction.reaction
|
||||
});
|
||||
export const renderLike = async (noteReaction: NoteReaction, note: Note) => {
|
||||
const reaction = noteReaction.reaction;
|
||||
|
||||
const object = {
|
||||
type: 'Like',
|
||||
id: `${config.url}/likes/${noteReaction.id}`,
|
||||
actor: `${config.url}/users/${noteReaction.userId}`,
|
||||
object: note.uri ? note.uri : `${config.url}/notes/${noteReaction.noteId}`,
|
||||
content: 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 { renderActivity } from '../../../remote/activitypub/renderer';
|
||||
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 { 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 { perUserReactionsChart } from '../../chart';
|
||||
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');
|
||||
}
|
||||
|
||||
reaction = await toDbReaction(reaction);
|
||||
reaction = await toDbReaction(reaction, user.host);
|
||||
|
||||
const exist = await NoteReactions.findOne({
|
||||
noteId: note.id,
|
||||
|
@ -59,8 +59,27 @@ export default async (user: User, note: Note, reaction?: string) => {
|
|||
|
||||
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', {
|
||||
reaction: reaction,
|
||||
emoji: emoji,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
|
@ -96,7 +115,7 @@ export default async (user: User, note: Note, reaction?: string) => {
|
|||
|
||||
//#region 配信
|
||||
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);
|
||||
if (note.userHost !== null) {
|
||||
const reactee = await Users.findOne(note.userId)
|
||||
|
|
|
@ -44,7 +44,7 @@ export default async (user: User, note: Note) => {
|
|||
|
||||
//#region 配信
|
||||
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);
|
||||
if (note.userHost !== null) {
|
||||
const reactee = await Users.findOne(note.userId)
|
||||
|
|
Loading…
Reference in New Issue