forked from FoundKeyGang/FoundKey
Compare commits
8 commits
main
...
account-mo
Author | SHA1 | Date | |
---|---|---|---|
Johann150 | 91aa08819a | ||
Johann150 | 3cda253152 | ||
Johann150 | 07488d6a32 | ||
Johann150 | d7db1bc34e | ||
Johann150 | abfba2699e | ||
Johann150 | ac3e807717 | ||
Johann150 | 1f7f978bbd | ||
Johann150 | 90756dfdfc |
|
@ -934,6 +934,7 @@ setTag: "Set tag"
|
||||||
addTag: "Add tag"
|
addTag: "Add tag"
|
||||||
removeTag: "Remove tag"
|
removeTag: "Remove tag"
|
||||||
externalCssSnippets: "Some CSS snippets for your inspiration (not managed by FoundKey)"
|
externalCssSnippets: "Some CSS snippets for your inspiration (not managed by FoundKey)"
|
||||||
|
movedTo: "This user has moved to {handle}."
|
||||||
_emailUnavailable:
|
_emailUnavailable:
|
||||||
used: "This email address is already being used"
|
used: "This email address is already being used"
|
||||||
format: "The format of this email address is invalid"
|
format: "The format of this email address is invalid"
|
||||||
|
@ -1476,6 +1477,7 @@ _notification:
|
||||||
yourFollowRequestAccepted: "Your follow request was accepted"
|
yourFollowRequestAccepted: "Your follow request was accepted"
|
||||||
youWereInvitedToGroup: "{userName} invited you to a group"
|
youWereInvitedToGroup: "{userName} invited you to a group"
|
||||||
pollEnded: "Poll results have become available"
|
pollEnded: "Poll results have become available"
|
||||||
|
moved: "{name} has moved to a different account"
|
||||||
emptyPushNotificationMessage: "Push notifications have been updated"
|
emptyPushNotificationMessage: "Push notifications have been updated"
|
||||||
_types:
|
_types:
|
||||||
all: "All"
|
all: "All"
|
||||||
|
@ -1490,6 +1492,7 @@ _notification:
|
||||||
receiveFollowRequest: "Received follow requests"
|
receiveFollowRequest: "Received follow requests"
|
||||||
followRequestAccepted: "Accepted follow requests"
|
followRequestAccepted: "Accepted follow requests"
|
||||||
groupInvited: "Group invitations"
|
groupInvited: "Group invitations"
|
||||||
|
move: "Others moving accounts"
|
||||||
app: "Notifications from linked apps"
|
app: "Notifications from linked apps"
|
||||||
_actions:
|
_actions:
|
||||||
followBack: "followed you back"
|
followBack: "followed you back"
|
||||||
|
|
BIN
packages/backend/assets/notification-badges/suitcase-solid.png
Normal file
BIN
packages/backend/assets/notification-badges/suitcase-solid.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
19
packages/backend/migration/1668977715500-movedTo.js
Normal file
19
packages/backend/migration/1668977715500-movedTo.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export class movedTo1668977715500 {
|
||||||
|
name = 'movedTo1668977715500';
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "movedToId" character varying(32)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "notification" ADD "moveTargetId" character varying(32)`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "notification"."moveTargetId" IS 'The ID of the moved to account.'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_16fef167e4253ccdc8971b01f6e" FOREIGN KEY ("movedToId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_078db271ad52ccc345b7b2b026a" FOREIGN KEY ("moveTargetId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_078db271ad52ccc345b7b2b026a"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_16fef167e4253ccdc8971b01f6e"`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "notification"."moveTargetId" IS 'The ID of the moved to account.'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "moveTargetId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "movedToId"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,19 +52,20 @@ export class Notification {
|
||||||
public notifier: User | null;
|
public notifier: User | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知の種類。
|
* Type of notification.
|
||||||
* follow - フォローされた
|
* follow - notifier followed notifiee
|
||||||
* mention - 投稿で自分が言及された
|
* mention - notifiee was mentioned
|
||||||
* reply - (自分または自分がWatchしている)投稿が返信された
|
* reply - notifiee (author or watching) was replied to
|
||||||
* renote - (自分または自分がWatchしている)投稿がRenoteされた
|
* renote - notifiee (author or watching) was renoted
|
||||||
* quote - (自分または自分がWatchしている)投稿が引用Renoteされた
|
* quote - notifiee (author or watching) was quoted
|
||||||
* reaction - (自分または自分がWatchしている)投稿にリアクションされた
|
* reaction - notifiee (author or watching) had a reaction added to the note
|
||||||
* pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された
|
* pollVote - new vote in a poll notifiee authored or watched
|
||||||
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
* pollEnded - notifiee's poll ended
|
||||||
* receiveFollowRequest - フォローリクエストされた
|
* receiveFollowRequest - notifiee received a new follow request
|
||||||
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
* followRequestAccepted - notifier accepted notifees follow request
|
||||||
* groupInvited - グループに招待された
|
* groupInvited - notifiee was invited into a group
|
||||||
* app - アプリ通知
|
* move - notifier moved
|
||||||
|
* app - custom application notification
|
||||||
*/
|
*/
|
||||||
@Index()
|
@Index()
|
||||||
@Column('enum', {
|
@Column('enum', {
|
||||||
|
@ -129,6 +130,19 @@ export class Notification {
|
||||||
})
|
})
|
||||||
public choice: number | null;
|
public choice: number | null;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
nullable: true,
|
||||||
|
comment: 'The ID of the moved to account.',
|
||||||
|
})
|
||||||
|
public moveTargetId: User['id'] | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public moveTarget: User | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* アプリ通知のbody
|
* アプリ通知のbody
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
import { Entity, Column, Index, OneToOne, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
||||||
import { id } from '../id.js';
|
import { id } from '../id.js';
|
||||||
import { DriveFile } from './drive-file.js';
|
import { DriveFile } from './drive-file.js';
|
||||||
|
|
||||||
|
@ -223,6 +223,18 @@ export class User {
|
||||||
})
|
})
|
||||||
public federateBlocks: boolean;
|
public federateBlocks: boolean;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public movedToId: User['id'] | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: 'SET NULL'
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public movedTo: User | null;
|
||||||
|
|
||||||
constructor(data: Partial<User>) {
|
constructor(data: Partial<User>) {
|
||||||
if (data == null) return;
|
if (data == null) return;
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,9 @@ export const NotificationRepository = db.getRepository(Notification).extend({
|
||||||
...(notification.type === 'groupInvited' ? {
|
...(notification.type === 'groupInvited' ? {
|
||||||
invitation: UserGroupInvitations.pack(notification.userGroupInvitationId!),
|
invitation: UserGroupInvitations.pack(notification.userGroupInvitationId!),
|
||||||
} : {}),
|
} : {}),
|
||||||
|
...(notification.type === 'move' ? {
|
||||||
|
moveTarget: Users.pack(notification.moveTarget ?? notification.moveTargetId),
|
||||||
|
} : {}),
|
||||||
...(notification.type === 'app' ? {
|
...(notification.type === 'app' ? {
|
||||||
body: notification.customBody,
|
body: notification.customBody,
|
||||||
header: notification.customHeader || token?.name,
|
header: notification.customHeader || token?.name,
|
||||||
|
|
|
@ -323,6 +323,10 @@ export const UserRepository = db.getRepository(User).extend({
|
||||||
}),
|
}),
|
||||||
emojis: populateEmojis(user.emojis, user.host),
|
emojis: populateEmojis(user.emojis, user.host),
|
||||||
onlineStatus: this.getOnlineStatus(user),
|
onlineStatus: this.getOnlineStatus(user),
|
||||||
|
movedTo: !user.movedToId ? undefined : this.pack(user.movedTo ?? user.movedToId, me, {
|
||||||
|
...opts,
|
||||||
|
detail: false,
|
||||||
|
}),
|
||||||
|
|
||||||
...(opts.detail ? {
|
...(opts.detail ? {
|
||||||
url: profile!.url,
|
url: profile!.url,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||||
import { toArray } from '@/prelude/array.js';
|
import { toArray } from '@/prelude/array.js';
|
||||||
import { apLogger } from '../logger.js';
|
import { apLogger } from '../logger.js';
|
||||||
import Resolver from '../resolver.js';
|
import Resolver from '../resolver.js';
|
||||||
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag } from '../type.js';
|
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag, isMove } from '../type.js';
|
||||||
import create from './create/index.js';
|
import create from './create/index.js';
|
||||||
import performDeleteActivity from './delete/index.js';
|
import performDeleteActivity from './delete/index.js';
|
||||||
import performUpdateActivity from './update/index.js';
|
import performUpdateActivity from './update/index.js';
|
||||||
|
@ -17,6 +17,7 @@ import add from './add/index.js';
|
||||||
import remove from './remove/index.js';
|
import remove from './remove/index.js';
|
||||||
import block from './block/index.js';
|
import block from './block/index.js';
|
||||||
import flag from './flag/index.js';
|
import flag from './flag/index.js';
|
||||||
|
import { move } from './move/index.js';
|
||||||
|
|
||||||
export async function performActivity(actor: CacheableRemoteUser, activity: IObject) {
|
export async function performActivity(actor: CacheableRemoteUser, activity: IObject) {
|
||||||
if (isCollectionOrOrderedCollection(activity)) {
|
if (isCollectionOrOrderedCollection(activity)) {
|
||||||
|
@ -67,6 +68,8 @@ async function performOneActivity(actor: CacheableRemoteUser, activity: IObject)
|
||||||
await block(actor, activity);
|
await block(actor, activity);
|
||||||
} else if (isFlag(activity)) {
|
} else if (isFlag(activity)) {
|
||||||
await flag(actor, activity);
|
await flag(actor, activity);
|
||||||
|
} else if (isMove(activity)) {
|
||||||
|
await move(actor, activity);
|
||||||
} else {
|
} else {
|
||||||
apLogger.warn(`unrecognized activity type: ${(activity as any).type}`);
|
apLogger.warn(`unrecognized activity type: ${(activity as any).type}`);
|
||||||
}
|
}
|
||||||
|
|
57
packages/backend/src/remote/activitypub/kernel/move/index.ts
Normal file
57
packages/backend/src/remote/activitypub/kernel/move/index.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||||
|
import { updatePerson } from '@/remote/activitypub/models/person.js';
|
||||||
|
import { Followings, Users } from '@/models/index.js';
|
||||||
|
import { createNotification } from '@/services/create-notification.js';
|
||||||
|
import Resolver from '../../resolver.js';
|
||||||
|
import { IMove, getApId } from '../../type.js';
|
||||||
|
|
||||||
|
export async function move(actor: CacheableRemoteUser, activity: IMove): Promise<void> {
|
||||||
|
const objectUri = getApId(activity);
|
||||||
|
|
||||||
|
// actor is not move origin
|
||||||
|
if (objectUri != actor.uri) return;
|
||||||
|
|
||||||
|
// actor already moved
|
||||||
|
if (actor.movedTo != null) return;
|
||||||
|
|
||||||
|
// no move target
|
||||||
|
if (activity.target == null) return;
|
||||||
|
|
||||||
|
const resolver = new Resolver();
|
||||||
|
|
||||||
|
/* the database resolver can not be used here, because:
|
||||||
|
* 1. It must be ensured that the latest data is used.
|
||||||
|
* 2. The AP representation is needed, because `alsoKnownAs`
|
||||||
|
* is not stored in the database.
|
||||||
|
*/
|
||||||
|
const movedToAp = resolver.resolve(activity.target);
|
||||||
|
|
||||||
|
// ensure the user exists
|
||||||
|
const movedTo = await resolvePerson(getApId(activity.target), resolver, movedToAp);
|
||||||
|
|
||||||
|
// move destination has not accepted
|
||||||
|
if (!Array.isArray(movedToAp.alsoKnownAs) || !moved_to.alsoKnownAs.includes(actor.id)) return;
|
||||||
|
|
||||||
|
// process move for local followers
|
||||||
|
const followingsQuery = Followings.createQueryBuilder('f')
|
||||||
|
.select('f.followerId')
|
||||||
|
.where('f.followeeId = :actorId', { actorId: actor.id })
|
||||||
|
.andWhere('f.followerHost IS NULL');
|
||||||
|
|
||||||
|
const followers = await UserProfiles.createQueryBuilder('profiles')
|
||||||
|
.where(`id IN (${ followingsQuery.getQuery() })`)
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
Users.update(actor.id, {
|
||||||
|
moved: movedTo.id,
|
||||||
|
}),
|
||||||
|
...followers.map(async (follower) => {
|
||||||
|
// TODO: autoAcceptMove?
|
||||||
|
|
||||||
|
await createNotification(follower.id, 'move', {
|
||||||
|
notifierId: actor.id,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
|
@ -42,7 +42,7 @@ const summaryLength = 2048;
|
||||||
* @param x Fetched object
|
* @param x Fetched object
|
||||||
* @param uri Fetch target URI
|
* @param uri Fetch target URI
|
||||||
*/
|
*/
|
||||||
function validateActor(x: IObject, uri: string): IActor {
|
async function validateActor(x: IObject, uri: string, resolver?: Resolver): IActor {
|
||||||
const expectHost = toPuny(new URL(uri).hostname);
|
const expectHost = toPuny(new URL(uri).hostname);
|
||||||
|
|
||||||
if (x == null) {
|
if (x == null) {
|
||||||
|
@ -57,6 +57,25 @@ function validateActor(x: IObject, uri: string): IActor {
|
||||||
throw new Error('invalid Actor: wrong id');
|
throw new Error('invalid Actor: wrong id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (x.movedTo !== undefined) {
|
||||||
|
if (!(typeof x.movedTo === 'string' && x.movedTo.length > 0)) {
|
||||||
|
throw new Error('invalid Actor: wrong movedTo');
|
||||||
|
}
|
||||||
|
if (x.movedTo === uri) {
|
||||||
|
throw new Error('invalid Actor: moved to self');
|
||||||
|
}
|
||||||
|
await resolvePerson(x.movedTo, resolver)
|
||||||
|
.then(moveTarget => {
|
||||||
|
x.movedTo = moveTarget.id
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
// Can't find the move target for some reason.
|
||||||
|
// Don't treat the actor as invalid, just ignore the movedTo.
|
||||||
|
logger.warn(`cannot find move target "${x.movedTo}", error: ${err.toString()}`);
|
||||||
|
delete x.movedTo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
|
if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
|
||||||
throw new Error('invalid Actor: wrong inbox');
|
throw new Error('invalid Actor: wrong inbox');
|
||||||
}
|
}
|
||||||
|
@ -134,16 +153,16 @@ export async function fetchPerson(uri: string, resolver?: Resolver): Promise<Cac
|
||||||
/**
|
/**
|
||||||
* Personを作成します。
|
* Personを作成します。
|
||||||
*/
|
*/
|
||||||
export async function createPerson(uri: string, resolver?: Resolver = new Resolver()): Promise<User> {
|
export async function createPerson(uri: string, resolver?: Resolver = new Resolver(), hint?: IObject): Promise<User> {
|
||||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||||
|
|
||||||
if (uri.startsWith(config.url)) {
|
if (uri.startsWith(config.url)) {
|
||||||
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
||||||
}
|
}
|
||||||
|
|
||||||
const object = await resolver.resolve(uri) as any;
|
const object = hint ?? await resolver.resolve(uri) as any;
|
||||||
|
|
||||||
const person = validateActor(object, uri);
|
const person = await validateActor(object, uri, resolver);
|
||||||
|
|
||||||
logger.info(`Creating the Person: ${person.id}`);
|
logger.info(`Creating the Person: ${person.id}`);
|
||||||
|
|
||||||
|
@ -183,6 +202,7 @@ export async function createPerson(uri: string, resolver?: Resolver = new Resolv
|
||||||
isBot,
|
isBot,
|
||||||
isCat: (person as any).isCat === true,
|
isCat: (person as any).isCat === true,
|
||||||
showTimelineReplies: false,
|
showTimelineReplies: false,
|
||||||
|
movedToId: person.movedTo,
|
||||||
})) as IRemoteUser;
|
})) as IRemoteUser;
|
||||||
|
|
||||||
await transactionalEntityManager.save(new UserProfile({
|
await transactionalEntityManager.save(new UserProfile({
|
||||||
|
@ -299,7 +319,7 @@ export async function updatePerson(uri: string, resolver?: Resolver = new Resolv
|
||||||
|
|
||||||
const object = hint || await resolver.resolve(uri);
|
const object = hint || await resolver.resolve(uri);
|
||||||
|
|
||||||
const person = validateActor(object, uri);
|
const person = await validateActor(object, uri, resolver);
|
||||||
|
|
||||||
logger.info(`Updating the Person: ${person.id}`);
|
logger.info(`Updating the Person: ${person.id}`);
|
||||||
|
|
||||||
|
@ -340,6 +360,7 @@ export async function updatePerson(uri: string, resolver?: Resolver = new Resolv
|
||||||
isCat: (person as any).isCat === true,
|
isCat: (person as any).isCat === true,
|
||||||
isLocked: !!person.manuallyApprovesFollowers,
|
isLocked: !!person.manuallyApprovesFollowers,
|
||||||
isExplorable: !!person.discoverable,
|
isExplorable: !!person.discoverable,
|
||||||
|
movedToId: person.movedTo,
|
||||||
} as Partial<User>;
|
} as Partial<User>;
|
||||||
|
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
|
@ -389,7 +410,7 @@ export async function updatePerson(uri: string, resolver?: Resolver = new Resolv
|
||||||
* If the target Person is registered in FoundKey, return it; otherwise, fetch it from a remote server and return it.
|
* If the target Person is registered in FoundKey, return it; otherwise, fetch it from a remote server and return it.
|
||||||
* Fetch the person from the remote server, register it in FoundKey, and return it.
|
* Fetch the person from the remote server, register it in FoundKey, and return it.
|
||||||
*/
|
*/
|
||||||
export async function resolvePerson(uri: string, resolver?: Resolver): Promise<CacheableUser> {
|
export async function resolvePerson(uri: string, resolver?: Resolver, hint?: IObject): Promise<CacheableUser> {
|
||||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||||
|
|
||||||
//#region このサーバーに既に登録されていたらそれを返す
|
//#region このサーバーに既に登録されていたらそれを返す
|
||||||
|
@ -401,7 +422,7 @@ export async function resolvePerson(uri: string, resolver?: Resolver): Promise<C
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// リモートサーバーからフェッチしてきて登録
|
// リモートサーバーからフェッチしてきて登録
|
||||||
return await createPerson(uri, resolver ?? new Resolver());
|
return await createPerson(uri, resolver ?? new Resolver(), hint);
|
||||||
}
|
}
|
||||||
|
|
||||||
const services: {
|
const services: {
|
||||||
|
|
|
@ -279,6 +279,10 @@ export interface IFlag extends IActivity {
|
||||||
type: 'Flag';
|
type: 'Flag';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IMove extends IActivity {
|
||||||
|
type: 'Move';
|
||||||
|
}
|
||||||
|
|
||||||
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
|
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
|
||||||
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
|
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
|
||||||
export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
|
export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
|
||||||
|
@ -293,3 +297,4 @@ export const isLike = (object: IObject): object is ILike => getApType(object) ==
|
||||||
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
|
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
|
||||||
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
||||||
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
||||||
|
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
<i v-else-if="notification.type === 'quote'" class="fas fa-quote-left"></i>
|
<i v-else-if="notification.type === 'quote'" class="fas fa-quote-left"></i>
|
||||||
<i v-else-if="notification.type === 'pollVote'" class="fas fa-poll-h"></i>
|
<i v-else-if="notification.type === 'pollVote'" class="fas fa-poll-h"></i>
|
||||||
<i v-else-if="notification.type === 'pollEnded'" class="fas fa-poll-h"></i>
|
<i v-else-if="notification.type === 'pollEnded'" class="fas fa-poll-h"></i>
|
||||||
|
<i v-else-if="notification.type === 'move'" class="fas fa-suitcase"></i>
|
||||||
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
||||||
<MkEmoji
|
<MkEmoji
|
||||||
v-else-if="notification.type === 'reaction'"
|
v-else-if="notification.type === 'reaction'"
|
||||||
|
@ -63,10 +64,27 @@
|
||||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full" :custom-emojis="notification.note.emojis"/>
|
||||||
<i class="fas fa-quote-right"></i>
|
<i class="fas fa-quote-right"></i>
|
||||||
</MkA>
|
</MkA>
|
||||||
<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
|
<span v-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">
|
||||||
|
{{ i18n.ts.youGotNewFollower }}
|
||||||
|
<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div>
|
||||||
|
</span>
|
||||||
<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
|
<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
|
||||||
<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span>
|
<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">
|
||||||
<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span>
|
{{ i18n.ts.receiveFollowRequest }}
|
||||||
|
<div v-if="full && !followRequestDone">
|
||||||
|
<button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">
|
||||||
|
{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b>
|
||||||
|
<div v-if="full && !groupInviteDone">
|
||||||
|
<button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<span v-if="notification.type === 'move'" class="text" style="opacity: 0.6;">
|
||||||
|
{{ i18n.ts.moved }}
|
||||||
|
<div v-if="full"><MkFollowButton :user="notification.moveTarget" :full="true"/></div>
|
||||||
|
</span>
|
||||||
<span v-if="notification.type === 'app'" class="text">
|
<span v-if="notification.type === 'app'" class="text">
|
||||||
<Mfm :text="notification.body" :nowrap="!full"/>
|
<Mfm :text="notification.body" :nowrap="!full"/>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -5,6 +5,11 @@
|
||||||
<!-- TODO -->
|
<!-- TODO -->
|
||||||
<!-- <div class="punished" v-if="user.isSuspended"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSuspended }}</div> -->
|
<!-- <div class="punished" v-if="user.isSuspended"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSuspended }}</div> -->
|
||||||
<!-- <div class="punished" v-if="user.isSilenced"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> -->
|
<!-- <div class="punished" v-if="user.isSilenced"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> -->
|
||||||
|
<div class="moved" v-if="user.movedTo"><i class="fas fa-suitcase"></i> <I18n :src="i18n.ts.movedTo" tag="span">
|
||||||
|
<template #handle>
|
||||||
|
<MkAcct :user="user.movedTo"/>
|
||||||
|
</template>
|
||||||
|
</I18n></div>
|
||||||
|
|
||||||
<div class="profile">
|
<div class="profile">
|
||||||
<MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/>
|
<MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/>
|
||||||
|
@ -186,7 +191,7 @@ onUnmounted(() => {
|
||||||
|
|
||||||
> .main {
|
> .main {
|
||||||
|
|
||||||
> .punished {
|
> .punished, .moved {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const;
|
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app'] as const;
|
||||||
|
|
||||||
export const noteNotificationTypes = ['mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded'] as const;
|
export const noteNotificationTypes = ['mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded'] as const;
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ export type UserLite = {
|
||||||
faviconUrl: Instance['faviconUrl'];
|
faviconUrl: Instance['faviconUrl'];
|
||||||
themeColor: Instance['themeColor'];
|
themeColor: Instance['themeColor'];
|
||||||
};
|
};
|
||||||
|
movedTo?: UserLite;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserDetailed = UserLite & {
|
export type UserDetailed = UserLite & {
|
||||||
|
@ -226,6 +227,11 @@ export type Notification = {
|
||||||
invitation: UserGroup;
|
invitation: UserGroup;
|
||||||
user: User;
|
user: User;
|
||||||
userId: User['id'];
|
userId: User['id'];
|
||||||
|
} | {
|
||||||
|
type: 'move',
|
||||||
|
user: User;
|
||||||
|
userId: User['id'];
|
||||||
|
moveTarget: User;
|
||||||
} | {
|
} | {
|
||||||
type: 'app';
|
type: 'app';
|
||||||
header?: string | null;
|
header?: string | null;
|
||||||
|
|
|
@ -218,6 +218,20 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
||||||
],
|
],
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
case 'move':
|
||||||
|
return [t('_notification.moved', { name: getUserName(data.body.user) }), {
|
||||||
|
body: getUserName(data.body.moveTarget),
|
||||||
|
icon: data.body.moveTarget.avatarUrl,
|
||||||
|
badge: iconUrl('suitcase'),
|
||||||
|
data,
|
||||||
|
action: [
|
||||||
|
{
|
||||||
|
action: 'accept',
|
||||||
|
title: t('follow'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
|
||||||
case 'app':
|
case 'app':
|
||||||
return [data.body.header || data.body.body, {
|
return [data.body.header || data.body.body, {
|
||||||
body: data.body.header && data.body.body,
|
body: data.body.header && data.body.body,
|
||||||
|
|
|
@ -104,6 +104,9 @@ self.addEventListener('notificationclick', <K extends keyof pushNotificationData
|
||||||
case 'groupInvited':
|
case 'groupInvited':
|
||||||
await swos.api('users/groups/invitations/accept', id, { invitationId: data.body.invitation.id });
|
await swos.api('users/groups/invitations/accept', id, { invitationId: data.body.invitation.id });
|
||||||
break;
|
break;
|
||||||
|
case 'move':
|
||||||
|
await swos.api('following/create', id, { userId: data.body.moveTarget.id });
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'reject':
|
case 'reject':
|
||||||
|
|
Loading…
Reference in a new issue