implement receiveing account moves (#309)
All checks were successful
ci/woodpecker/push/lint-foundkey-js Pipeline was successful
ci/woodpecker/push/lint-backend Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint-client Pipeline was successful
ci/woodpecker/push/lint-sw Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
All checks were successful
ci/woodpecker/push/lint-foundkey-js Pipeline was successful
ci/woodpecker/push/lint-backend Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/lint-client Pipeline was successful
ci/woodpecker/push/lint-sw Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
Reviewed-on: #309 Changelog: Added
This commit is contained in:
commit
bc51450fea
17 changed files with 225 additions and 21 deletions
|
@ -829,6 +829,7 @@ oauthErrorGoBack: "An error happened while trying to authenticate a 3rd party ap
|
|||
\ Please go back and try again."
|
||||
appAuthorization: "App authorization"
|
||||
noPermissionsRequested: "(No permissions requested.)"
|
||||
movedTo: "This user has moved to {handle}."
|
||||
_emailUnavailable:
|
||||
used: "This email address is already being used"
|
||||
format: "The format of this email address is invalid"
|
||||
|
@ -1306,6 +1307,7 @@ _notification:
|
|||
receiveFollowRequest: "Received follow requests"
|
||||
followRequestAccepted: "Accepted follow requests"
|
||||
groupInvited: "Group invitations"
|
||||
move: "Others moving accounts"
|
||||
app: "Notifications from linked apps"
|
||||
_actions:
|
||||
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 |
35
packages/backend/migration/1668977715500-movedTo.js
Normal file
35
packages/backend/migration/1668977715500-movedTo.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
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`);
|
||||
await queryRunner.query(`ALTER TYPE "notification_type_enum" ADD VALUE 'move'`);
|
||||
await queryRunner.query(`ALTER TYPE "user_profile_mutingnotificationtypes_enum" ADD VALUE 'move'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
// remove 'move' from user muting notifications type enum
|
||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'pollEnded')`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`);
|
||||
|
||||
// remove 'move' from notification type enum
|
||||
await queryRunner.query(`DELETE FROM "notification" WHERE "type" = 'move'`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_078db271ad52ccc345b7b2b026a"`);
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_16fef167e4253ccdc8971b01f6e"`);
|
||||
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;
|
||||
|
||||
/**
|
||||
* 通知の種類。
|
||||
* follow - フォローされた
|
||||
* mention - 投稿で自分が言及された
|
||||
* reply - (自分または自分がWatchしている)投稿が返信された
|
||||
* renote - (自分または自分がWatchしている)投稿がRenoteされた
|
||||
* quote - (自分または自分がWatchしている)投稿が引用Renoteされた
|
||||
* reaction - (自分または自分がWatchしている)投稿にリアクションされた
|
||||
* pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された
|
||||
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
||||
* receiveFollowRequest - フォローリクエストされた
|
||||
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
||||
* groupInvited - グループに招待された
|
||||
* app - アプリ通知
|
||||
* Type of notification.
|
||||
* follow - notifier followed notifiee
|
||||
* mention - notifiee was mentioned
|
||||
* reply - notifiee (author or watching) was replied to
|
||||
* renote - notifiee (author or watching) was renoted
|
||||
* quote - notifiee (author or watching) was quoted
|
||||
* reaction - notifiee (author or watching) had a reaction added to the note
|
||||
* pollVote - new vote in a poll notifiee authored or watched
|
||||
* pollEnded - notifiee's poll ended
|
||||
* receiveFollowRequest - notifiee received a new follow request
|
||||
* followRequestAccepted - notifier accepted notifees follow request
|
||||
* groupInvited - notifiee was invited into a group
|
||||
* move - notifier moved
|
||||
* app - custom application notification
|
||||
*/
|
||||
@Index()
|
||||
@Column('enum', {
|
||||
|
@ -129,6 +130,19 @@ export class Notification {
|
|||
})
|
||||
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
|
||||
*/
|
||||
|
|
|
@ -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 { DriveFile } from './drive-file.js';
|
||||
|
||||
|
@ -230,6 +230,18 @@ export class User {
|
|||
})
|
||||
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>) {
|
||||
if (data == null) return;
|
||||
|
||||
|
|
|
@ -44,6 +44,9 @@ export const NotificationRepository = db.getRepository(Notification).extend({
|
|||
...(notification.type === 'groupInvited' ? {
|
||||
invitation: UserGroupInvitations.pack(notification.userGroupInvitationId!),
|
||||
} : {}),
|
||||
...(notification.type === 'move' ? {
|
||||
moveTarget: Users.pack(notification.moveTarget ?? notification.moveTargetId),
|
||||
} : {}),
|
||||
...(notification.type === 'app' ? {
|
||||
body: notification.customBody,
|
||||
header: notification.customHeader || token?.name,
|
||||
|
|
|
@ -300,6 +300,10 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
}),
|
||||
emojis: populateEmojis(user.emojis, user.host),
|
||||
onlineStatus: this.getOnlineStatus(user),
|
||||
movedTo: !user.movedToId ? undefined : this.pack(user.movedTo ?? user.movedToId, me, {
|
||||
...opts,
|
||||
detail: false,
|
||||
}),
|
||||
|
||||
...(opts.detail ? {
|
||||
url: profile!.url,
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Resolver } from '@/remote/activitypub/resolver.js';
|
|||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { shouldBlockInstance } from '@/misc/should-block-instance.js';
|
||||
import { apLogger } from '../logger.js';
|
||||
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag, getApId } from '../type.js';
|
||||
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag, isMove, getApId } from '../type.js';
|
||||
import create from './create/index.js';
|
||||
import performDeleteActivity from './delete/index.js';
|
||||
import performUpdateActivity from './update/index.js';
|
||||
|
@ -19,6 +19,7 @@ import add from './add/index.js';
|
|||
import remove from './remove/index.js';
|
||||
import block from './block/index.js';
|
||||
import flag from './flag/index.js';
|
||||
import { move } from './move/index.js';
|
||||
|
||||
export async function performActivity(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
|
||||
if (isCollectionOrOrderedCollection(activity)) {
|
||||
|
@ -73,6 +74,8 @@ async function performOneActivity(actor: CacheableRemoteUser, activity: IObject,
|
|||
await block(actor, activity);
|
||||
} else if (isFlag(activity)) {
|
||||
await flag(actor, activity);
|
||||
} else if (isMove(activity)) {
|
||||
await move(actor, activity, resolver);
|
||||
} else {
|
||||
apLogger.warn(`unrecognized activity type: ${(activity as any).type}`);
|
||||
}
|
||||
|
|
62
packages/backend/src/remote/activitypub/kernel/move/index.ts
Normal file
62
packages/backend/src/remote/activitypub/kernel/move/index.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { IsNull } from 'typeorm';
|
||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import { resolvePerson } 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, isActor, getApId } from '../../type.js';
|
||||
|
||||
export async function move(actor: CacheableRemoteUser, activity: IMove, resolver: Resolver): Promise<void> {
|
||||
// actor is not move origin
|
||||
if (activity.object == null || getApId(activity.object) !== actor.uri) return;
|
||||
|
||||
// actor already moved
|
||||
if (actor.movedTo != null) return;
|
||||
|
||||
// no move target
|
||||
if (activity.target == null) return;
|
||||
|
||||
/* 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.
|
||||
* This also checks whether the move target is blocked
|
||||
*/
|
||||
const movedToAp = await resolver.resolve(getApId(activity.target));
|
||||
|
||||
// move target is not an actor
|
||||
if (!isActor(movedToAp)) return;
|
||||
|
||||
// move destination has not accepted
|
||||
if (!Array.isArray(movedToAp.alsoKnownAs) || !movedToAp.alsoKnownAs.includes(actor.id)) return;
|
||||
|
||||
// ensure the user exists
|
||||
const movedTo = await resolvePerson(getApId(activity.target), resolver, movedToAp);
|
||||
// move target is already suspended
|
||||
if (movedTo.isSuspended) return;
|
||||
|
||||
// process move for local followers
|
||||
const followings = Followings.find({
|
||||
select: {
|
||||
followerId: true,
|
||||
},
|
||||
where: {
|
||||
followeeId: actor.id,
|
||||
followerHost: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
Users.update(actor.id, {
|
||||
movedToId: movedTo.id,
|
||||
}),
|
||||
...followings.map(async (following) => {
|
||||
// TODO: autoAcceptMove?
|
||||
|
||||
await createNotification(following.followerId, 'move', {
|
||||
notifierId: actor.id,
|
||||
moveTargetId: movedTo.id,
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
|
@ -39,7 +39,7 @@ const summaryLength = 2048;
|
|||
* @param x Fetched object
|
||||
* @param uri Fetch target URI
|
||||
*/
|
||||
function validateActor(x: IObject): IActor {
|
||||
async function validateActor(x: IObject, resolver: Resolver): Promise<IActor> {
|
||||
if (x == null) {
|
||||
throw new Error('invalid Actor: object is null');
|
||||
}
|
||||
|
@ -61,6 +61,22 @@ function validateActor(x: IObject): IActor {
|
|||
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
// This may throw an exception if we cannot resolve the move target.
|
||||
// If we are processing an incoming activity, this is desired behaviour
|
||||
// because that will cause the activity to be retried.
|
||||
await resolvePerson(x.movedTo, resolver)
|
||||
.then(moveTarget => {
|
||||
x.movedTo = moveTarget.id
|
||||
});
|
||||
}
|
||||
|
||||
if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
|
||||
throw new Error('invalid Actor: wrong inbox');
|
||||
}
|
||||
|
@ -137,7 +153,7 @@ export async function fetchPerson(uri: string): Promise<CacheableUser | null> {
|
|||
export async function createPerson(value: string | IObject, resolver: Resolver): Promise<User> {
|
||||
const object = await resolver.resolve(value) as any;
|
||||
|
||||
const person = validateActor(object);
|
||||
const person = await validateActor(object, resolver);
|
||||
|
||||
apLogger.info(`Creating the Person: ${person.id}`);
|
||||
|
||||
|
@ -177,6 +193,7 @@ export async function createPerson(value: string | IObject, resolver: Resolver):
|
|||
isBot,
|
||||
isCat: (person as any).isCat === true,
|
||||
showTimelineReplies: false,
|
||||
movedToId: person.movedTo,
|
||||
})) as IRemoteUser;
|
||||
|
||||
await transactionalEntityManager.save(new UserProfile({
|
||||
|
@ -287,7 +304,7 @@ export async function updatePerson(value: IObject | string, resolver: Resolver):
|
|||
|
||||
const object = await resolver.resolve(value);
|
||||
|
||||
const person = validateActor(object);
|
||||
const person = await validateActor(object, resolver);
|
||||
|
||||
apLogger.info(`Updating the Person: ${person.id}`);
|
||||
|
||||
|
@ -328,6 +345,7 @@ export async function updatePerson(value: IObject | string, resolver: Resolver):
|
|||
isCat: (person as any).isCat === true,
|
||||
isLocked: !!person.manuallyApprovesFollowers,
|
||||
isExplorable: !!person.discoverable,
|
||||
movedToId: person.movedTo,
|
||||
} as Partial<User>;
|
||||
|
||||
if (avatar) {
|
||||
|
@ -376,7 +394,7 @@ export async function updatePerson(value: IObject | string, resolver: Resolver):
|
|||
* 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.
|
||||
*/
|
||||
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');
|
||||
|
||||
//#region このサーバーに既に登録されていたらそれを返す
|
||||
|
@ -388,7 +406,7 @@ export async function resolvePerson(uri: string, resolver: Resolver): Promise<Ca
|
|||
//#endregion
|
||||
|
||||
// リモートサーバーからフェッチしてきて登録
|
||||
return await createPerson(uri, resolver);
|
||||
return await createPerson(hint ?? uri, resolver);
|
||||
}
|
||||
|
||||
export function analyzeAttachments(attachments: IObject | IObject[] | undefined) {
|
||||
|
|
|
@ -296,6 +296,10 @@ export interface IFlag extends IActivity {
|
|||
type: 'Flag';
|
||||
}
|
||||
|
||||
export interface IMove extends IActivity {
|
||||
type: 'Move';
|
||||
}
|
||||
|
||||
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
|
||||
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
|
||||
export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
|
||||
|
@ -310,6 +314,7 @@ export const isLike = (object: IObject): object is ILike => getApType(object) ==
|
|||
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
|
||||
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
||||
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
||||
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
|
||||
|
||||
export interface ILink {
|
||||
href: string;
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
<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 === 'pollEnded'" class="fas fa-poll-h"></i>
|
||||
<i v-else-if="notification.type === 'move'" class="fas fa-suitcase"></i>
|
||||
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
||||
<MkEmoji
|
||||
v-else-if="notification.type === 'reaction'"
|
||||
|
@ -86,6 +87,14 @@
|
|||
<button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button>
|
||||
</div>
|
||||
</span>
|
||||
<span v-if="notification.type === 'move'" class="text" style="opacity: 0.6;">
|
||||
<I18n :src="i18n.ts.movedTo">
|
||||
<template #handle>
|
||||
<MkAcct :user="notification.moveTarget"/>
|
||||
</template>
|
||||
</I18n>
|
||||
<div v-if="full"><MkFollowButton :user="notification.moveTarget" :full="true"/></div>
|
||||
</span>
|
||||
<span v-if="notification.type === 'app'" class="text">
|
||||
<Mfm :text="notification.body" :nowrap="!full"/>
|
||||
</span>
|
||||
|
|
|
@ -9,6 +9,16 @@
|
|||
<div class="profile">
|
||||
<MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/>
|
||||
|
||||
<MkInfo v-if="user.movedTo" class="moved">
|
||||
<I18n :src="i18n.ts.movedTo" tag="span">
|
||||
<template #handle>
|
||||
<MkA :to="userPage(user.movedTo)" style="color: var(--accent);">
|
||||
<MkAcct :user="user.movedTo"/>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
</MkInfo>
|
||||
|
||||
<div :key="user.id" class="_block main">
|
||||
<div class="banner-container" :style="style">
|
||||
<div ref="bannerEl" class="banner" :style="style"></div>
|
||||
|
@ -193,6 +203,10 @@ onUnmounted(() => {
|
|||
|
||||
> .profile {
|
||||
|
||||
> .moved {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
> .main {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ export type UserLite = {
|
|||
faviconUrl: Instance['faviconUrl'];
|
||||
themeColor: Instance['themeColor'];
|
||||
};
|
||||
movedTo?: UserLite;
|
||||
};
|
||||
|
||||
export type UserDetailed = UserLite & {
|
||||
|
@ -223,6 +224,11 @@ export type Notification = {
|
|||
invitation: UserGroup;
|
||||
user: User;
|
||||
userId: User['id'];
|
||||
} | {
|
||||
type: 'move',
|
||||
user: User;
|
||||
userId: User['id'];
|
||||
moveTarget: User;
|
||||
} | {
|
||||
type: 'app';
|
||||
header?: string | null;
|
||||
|
|
|
@ -217,6 +217,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':
|
||||
return [data.body.header || data.body.body, {
|
||||
body: data.body.header && data.body.body,
|
||||
|
|
|
@ -96,6 +96,9 @@ self.addEventListener('notificationclick', <K extends keyof pushNotificationData
|
|||
case 'groupInvited':
|
||||
await swos.api('users/groups/invitations/accept', id, { invitationId: data.body.invitation.id });
|
||||
break;
|
||||
case 'move':
|
||||
await swos.api('following/create', id, { userId: data.body.moveTarget.id });
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'reject':
|
||||
|
|
Loading…
Reference in a new issue