mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-21 17:37:29 -07:00
apply patches
This commit is contained in:
parent
85cdfb8f33
commit
ef5d46e2cb
29 changed files with 688 additions and 1 deletions
|
@ -1132,6 +1132,11 @@ openInMainColumn: "Open in main column"
|
||||||
searchNotLoggedIn_1: "You have to be authenticated in order to use full text search."
|
searchNotLoggedIn_1: "You have to be authenticated in order to use full text search."
|
||||||
searchNotLoggedIn_2: "However, you can search using hashtags, and search users."
|
searchNotLoggedIn_2: "However, you can search using hashtags, and search users."
|
||||||
searchEmptyQuery: "Please enter a search term."
|
searchEmptyQuery: "Please enter a search term."
|
||||||
|
bite: "Bite"
|
||||||
|
biteBack: "Bite back"
|
||||||
|
bittenBack: "Bitten back"
|
||||||
|
bitYou: "bit you"
|
||||||
|
bitYouBack: "bit you back"
|
||||||
|
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "Reduces the effort of server moderation through automatically recognizing
|
description: "Reduces the effort of server moderation through automatically recognizing
|
||||||
|
@ -2108,6 +2113,7 @@ _notification:
|
||||||
followRequestAccepted: "Accepted follow requests"
|
followRequestAccepted: "Accepted follow requests"
|
||||||
groupInvited: "Group invitations"
|
groupInvited: "Group invitations"
|
||||||
app: "Notifications from linked apps"
|
app: "Notifications from linked apps"
|
||||||
|
bite: "Bites"
|
||||||
_actions:
|
_actions:
|
||||||
followBack: "followed you back"
|
followBack: "followed you back"
|
||||||
reply: "Reply"
|
reply: "Reply"
|
||||||
|
|
|
@ -76,6 +76,7 @@ import { OAuthApp } from "@/models/entities/oauth-app.js";
|
||||||
import { OAuthToken } from "@/models/entities/oauth-token.js";
|
import { OAuthToken } from "@/models/entities/oauth-token.js";
|
||||||
import { HtmlNoteCacheEntry } from "@/models/entities/html-note-cache-entry.js";
|
import { HtmlNoteCacheEntry } from "@/models/entities/html-note-cache-entry.js";
|
||||||
import { HtmlUserCacheEntry } from "@/models/entities/html-user-cache-entry.js";
|
import { HtmlUserCacheEntry } from "@/models/entities/html-user-cache-entry.js";
|
||||||
|
import { Bite } from "@/models/entities/bite.js";
|
||||||
|
|
||||||
const sqlLogger = dbLogger.createSubLogger("sql", "gray", false);
|
const sqlLogger = dbLogger.createSubLogger("sql", "gray", false);
|
||||||
class MyCustomLogger implements Logger {
|
class MyCustomLogger implements Logger {
|
||||||
|
@ -179,6 +180,7 @@ export const entities = [
|
||||||
OAuthToken,
|
OAuthToken,
|
||||||
HtmlNoteCacheEntry,
|
HtmlNoteCacheEntry,
|
||||||
HtmlUserCacheEntry,
|
HtmlUserCacheEntry,
|
||||||
|
Bite,
|
||||||
...charts,
|
...charts,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class FederatedBite1705528046452 implements MigrationInterface {
|
||||||
|
name = 'FederatedBite1705528046452'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TYPE "public"."bite_targettype_enum" AS ENUM('user', 'bite')`);
|
||||||
|
await queryRunner.query(`CREATE TABLE "bite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "uri" character varying(512), "userId" character varying(32) NOT NULL, "targetType" "public"."bite_targettype_enum" NOT NULL, "targetUserId" character varying(32), "targetBiteId" character varying(32), "replied" boolean NOT NULL DEFAULT true, CONSTRAINT "CHK_c3a20c5756ccff3133f8927500" CHECK ("targetUserId" IS NOT NULL OR "targetBiteId" IS NOT NULL), CONSTRAINT "PK_1887f3f621a4a7655a1b78bfd66" PRIMARY KEY ("id")); COMMENT ON COLUMN "bite"."uri" IS 'null if local'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "notification" ADD "biteId" character varying(32)`);
|
||||||
|
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`);
|
||||||
|
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'bite')`);
|
||||||
|
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"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||||
|
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum_old"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "bite" ADD CONSTRAINT "FK_8d00aa79e157364ac1f60c15098" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "bite" ADD CONSTRAINT "FK_a646fbbeb6efa2531c75fec46b9" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "bite" ADD CONSTRAINT "FK_5d5f68610583f2e0b6785d3c0e9" FOREIGN KEY ("targetBiteId") REFERENCES "bite"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_c54844158c1eead7042e7ca4c83" FOREIGN KEY ("biteId") REFERENCES "bite"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`);
|
||||||
|
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'bite')`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`);
|
||||||
|
await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_c54844158c1eead7042e7ca4c83"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "bite" DROP CONSTRAINT "FK_5d5f68610583f2e0b6785d3c0e9"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "bite" DROP CONSTRAINT "FK_a646fbbeb6efa2531c75fec46b9"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "bite" DROP CONSTRAINT "FK_8d00aa79e157364ac1f60c15098"`);
|
||||||
|
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
|
||||||
|
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"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "biteId"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "bite"`);
|
||||||
|
await queryRunner.query(`DROP TYPE "public"."bite_targettype_enum"`);
|
||||||
|
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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ import { packedQueueCountSchema } from "@/models/schema/queue.js";
|
||||||
import { packedGalleryPostSchema } from "@/models/schema/gallery-post.js";
|
import { packedGalleryPostSchema } from "@/models/schema/gallery-post.js";
|
||||||
import { packedEmojiSchema } from "@/models/schema/emoji.js";
|
import { packedEmojiSchema } from "@/models/schema/emoji.js";
|
||||||
import { packedNoteEdit } from "@/models/schema/note-edit.js";
|
import { packedNoteEdit } from "@/models/schema/note-edit.js";
|
||||||
|
import { packedBiteSchema } from "@/models/schema/bite.js";
|
||||||
|
|
||||||
export const refs = {
|
export const refs = {
|
||||||
UserLite: packedUserLiteSchema,
|
UserLite: packedUserLiteSchema,
|
||||||
|
@ -65,6 +66,7 @@ export const refs = {
|
||||||
FederationInstance: packedFederationInstanceSchema,
|
FederationInstance: packedFederationInstanceSchema,
|
||||||
GalleryPost: packedGalleryPostSchema,
|
GalleryPost: packedGalleryPostSchema,
|
||||||
Emoji: packedEmojiSchema,
|
Emoji: packedEmojiSchema,
|
||||||
|
Bite: packedBiteSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||||
|
|
56
packages/backend/src/models/entities/bite.ts
Normal file
56
packages/backend/src/models/entities/bite.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { Check, Column, Entity, ManyToOne, PrimaryColumn } from "typeorm";
|
||||||
|
import { id } from "../id.js";
|
||||||
|
import { User } from "./user.js";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Check(`"targetUserId" IS NOT NULL OR "targetBiteId" IS NOT NULL`)
|
||||||
|
export class Bite {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@Column("timestamp with time zone")
|
||||||
|
public createdAt: Date;
|
||||||
|
|
||||||
|
@Column("varchar", {
|
||||||
|
length: 512,
|
||||||
|
nullable: true,
|
||||||
|
comment: "null if local",
|
||||||
|
})
|
||||||
|
public uri: string | null;
|
||||||
|
|
||||||
|
@Column(id())
|
||||||
|
public userId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
})
|
||||||
|
public user: User;
|
||||||
|
|
||||||
|
@Column("enum", {
|
||||||
|
enum: ["user", "bite"],
|
||||||
|
})
|
||||||
|
public targetType: "user" | "bite";
|
||||||
|
|
||||||
|
@Column({ ...id(), nullable: true })
|
||||||
|
public targetUserId: string | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public targetUser: User | null;
|
||||||
|
|
||||||
|
@Column({ ...id(), nullable: true })
|
||||||
|
public targetBiteId: string | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => Bite, {
|
||||||
|
onDelete: "CASCADE",
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public targetBite: Bite | null;
|
||||||
|
|
||||||
|
@Column("boolean", {
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
public replied: boolean;
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import { FollowRequest } from "./follow-request.js";
|
||||||
import { UserGroupInvitation } from "./user-group-invitation.js";
|
import { UserGroupInvitation } from "./user-group-invitation.js";
|
||||||
import { AccessToken } from "./access-token.js";
|
import { AccessToken } from "./access-token.js";
|
||||||
import { notificationTypes } from "@/types.js";
|
import { notificationTypes } from "@/types.js";
|
||||||
|
import { Bite } from "./bite.js";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Notification {
|
export class Notification {
|
||||||
|
@ -181,4 +182,12 @@ export class Notification {
|
||||||
})
|
})
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public appAccessToken: AccessToken | null;
|
public appAccessToken: AccessToken | null;
|
||||||
|
|
||||||
|
@Column({ ...id(), nullable: true })
|
||||||
|
public biteId: Bite["id"] | null;
|
||||||
|
|
||||||
|
@ManyToOne((type) => Bite, {
|
||||||
|
onDelete: "CASCADE", nullable: true
|
||||||
|
})
|
||||||
|
public bite: Bite | null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,7 @@ import { OAuthToken } from "@/models/entities/oauth-token.js";
|
||||||
import { UserProfileRepository } from "@/models/repositories/user-profile.js";
|
import { UserProfileRepository } from "@/models/repositories/user-profile.js";
|
||||||
import { HtmlNoteCacheEntry } from "@/models/entities/html-note-cache-entry.js";
|
import { HtmlNoteCacheEntry } from "@/models/entities/html-note-cache-entry.js";
|
||||||
import { HtmlUserCacheEntry } from "@/models/entities/html-user-cache-entry.js";
|
import { HtmlUserCacheEntry } from "@/models/entities/html-user-cache-entry.js";
|
||||||
|
import { BiteRespository } from "./repositories/bite.js";
|
||||||
|
|
||||||
export const Announcements = db.getRepository(Announcement);
|
export const Announcements = db.getRepository(Announcement);
|
||||||
export const AnnouncementReads = db.getRepository(AnnouncementRead);
|
export const AnnouncementReads = db.getRepository(AnnouncementRead);
|
||||||
|
@ -138,3 +139,4 @@ export const OAuthApps = db.getRepository(OAuthApp);
|
||||||
export const OAuthTokens = db.getRepository(OAuthToken);
|
export const OAuthTokens = db.getRepository(OAuthToken);
|
||||||
export const HtmlUserCacheEntries = db.getRepository(HtmlUserCacheEntry);
|
export const HtmlUserCacheEntries = db.getRepository(HtmlUserCacheEntry);
|
||||||
export const HtmlNoteCacheEntries = db.getRepository(HtmlNoteCacheEntry);
|
export const HtmlNoteCacheEntries = db.getRepository(HtmlNoteCacheEntry);
|
||||||
|
export const Bites = BiteRespository;
|
||||||
|
|
71
packages/backend/src/models/repositories/bite.ts
Normal file
71
packages/backend/src/models/repositories/bite.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { db } from "@/db/postgre.js";
|
||||||
|
import { Bite } from "../entities/bite.js";
|
||||||
|
import { Packed } from "@/misc/schema.js";
|
||||||
|
import { Bites, Users } from "../index.js";
|
||||||
|
import { User } from "../entities/user.js";
|
||||||
|
import { awaitAll } from "@/prelude/await-all.js";
|
||||||
|
import config from "@/config/index.js";
|
||||||
|
|
||||||
|
export const BiteRespository = db.getRepository(Bite).extend({
|
||||||
|
async pack(
|
||||||
|
src: Bite | Bite["id"],
|
||||||
|
me?: { id: User["id"] } | null | undefined,
|
||||||
|
): Promise<Packed<"Bite">> {
|
||||||
|
const bite =
|
||||||
|
typeof src === "object" ? src : await this.findOneByOrFail({ id: src });
|
||||||
|
return await awaitAll({
|
||||||
|
id: bite.id,
|
||||||
|
user: Users.pack(bite.user ?? bite.userId, me, { detail: false }),
|
||||||
|
targetType: bite.targetType,
|
||||||
|
target: this.packTarget(bite, me),
|
||||||
|
replied: bite.replied,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async packTarget(
|
||||||
|
bite: Bite,
|
||||||
|
me?: { id: User["id"] } | null | undefined,
|
||||||
|
): Promise<Packed<"UserLite"> | Packed<"Bite">> {
|
||||||
|
switch (bite.targetType) {
|
||||||
|
case "user":
|
||||||
|
return await Users.pack(bite.targetUser ?? bite.targetUserId!, me, {
|
||||||
|
detail: false,
|
||||||
|
});
|
||||||
|
case "bite":
|
||||||
|
return await this.pack(bite.targetBite ?? bite.targetBiteId!, me);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async targetUri(bite: Bite): Promise<string> {
|
||||||
|
switch (bite.targetType) {
|
||||||
|
case "user": {
|
||||||
|
bite.targetUser =
|
||||||
|
bite.targetUser ??
|
||||||
|
(await Users.findOneOrFail({ where: { id: bite.targetUserId! } }));
|
||||||
|
return (
|
||||||
|
bite.targetUser.uri || `${config.url}/users/${bite.targetUserId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "bite": {
|
||||||
|
bite.targetBite =
|
||||||
|
bite.targetBite ??
|
||||||
|
(await Bites.findOneOrFail({ where: { id: bite.targetBiteId! } }));
|
||||||
|
return (
|
||||||
|
bite.targetBite.uri || `${config.url}/bites/${bite.targetBiteId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async targetUserId(bite: Bite): Promise<User["id"]> {
|
||||||
|
switch (bite.targetType) {
|
||||||
|
case "user":
|
||||||
|
return bite.targetUserId!;
|
||||||
|
case "bite":
|
||||||
|
bite.targetBite =
|
||||||
|
bite.targetBite ??
|
||||||
|
(await Bites.findOneByOrFail({ id: bite.targetBiteId! }));
|
||||||
|
return bite.targetBite.userId;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
|
@ -14,6 +14,7 @@ import {
|
||||||
UserGroupInvitations,
|
UserGroupInvitations,
|
||||||
AccessTokens,
|
AccessTokens,
|
||||||
NoteReactions,
|
NoteReactions,
|
||||||
|
Bites,
|
||||||
} from "../index.js";
|
} from "../index.js";
|
||||||
|
|
||||||
export const NotificationRepository = db.getRepository(Notification).extend({
|
export const NotificationRepository = db.getRepository(Notification).extend({
|
||||||
|
@ -143,6 +144,11 @@ export const NotificationRepository = db.getRepository(Notification).extend({
|
||||||
icon: notification.customIcon || token?.iconUrl,
|
icon: notification.customIcon || token?.iconUrl,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
...(notification.type === "bite"
|
||||||
|
? {
|
||||||
|
bite: notification.bite ?? await Bites.findOneBy({ id: notification.biteId! }),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
31
packages/backend/src/models/schema/bite.ts
Normal file
31
packages/backend/src/models/schema/bite.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
export const packedBiteSchema = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: "string",
|
||||||
|
format: "id",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: "object",
|
||||||
|
ref: "UserLite",
|
||||||
|
},
|
||||||
|
targetType: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["user", "bite"],
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
type: "object",
|
||||||
|
ref: "UserLite",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "object",
|
||||||
|
ref: "Bite",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
|
@ -75,5 +75,11 @@ export const packedNotificationSchema = {
|
||||||
optional: true,
|
optional: true,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
},
|
},
|
||||||
|
bite: {
|
||||||
|
type: "object",
|
||||||
|
ref: "Bite",
|
||||||
|
optional: true,
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
79
packages/backend/src/remote/activitypub/kernel/bite.ts
Normal file
79
packages/backend/src/remote/activitypub/kernel/bite.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import { CacheableRemoteUser } from "@/models/entities/user.js";
|
||||||
|
import { IActivity, IBite } from "../type.js";
|
||||||
|
import Resolver from "../resolver.js";
|
||||||
|
import { fetchPerson } from "../models/person.js";
|
||||||
|
import config from "@/config/index.js";
|
||||||
|
import { genId } from "@/misc/gen-id.js";
|
||||||
|
import { createBite } from "@/services/create-bite.js";
|
||||||
|
import { Bite } from "@/models/entities/bite.js";
|
||||||
|
|
||||||
|
export default async (
|
||||||
|
actor: CacheableRemoteUser,
|
||||||
|
bite: IBite,
|
||||||
|
): Promise<string> => {
|
||||||
|
if (actor.uri !== bite.actor) {
|
||||||
|
return "skip: actor uri mismatch";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bite.id === null) {
|
||||||
|
return "skip: bite id not specified";
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolver = new Resolver();
|
||||||
|
const biteActor = await fetchPerson(bite.actor, resolver);
|
||||||
|
if (biteActor === null) {
|
||||||
|
return "skip: biteActor is null";
|
||||||
|
}
|
||||||
|
if (!bite.target.startsWith(`${config.url}/`)) {
|
||||||
|
return "skip: target is not local";
|
||||||
|
}
|
||||||
|
|
||||||
|
const localId = genId();
|
||||||
|
const fields = {
|
||||||
|
id: localId,
|
||||||
|
userId: biteActor.id,
|
||||||
|
replied: false,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const parts = bite.target.split("/");
|
||||||
|
const targetDbId = parts.pop();
|
||||||
|
const targetPathType = parts.pop();
|
||||||
|
|
||||||
|
let targetType: Bite["targetType"];
|
||||||
|
let targetId;
|
||||||
|
|
||||||
|
if (targetPathType === "users") {
|
||||||
|
targetType = "user";
|
||||||
|
targetId = targetDbId;
|
||||||
|
} else if (targetPathType === "bites") {
|
||||||
|
targetType = "bite";
|
||||||
|
targetId = targetDbId;
|
||||||
|
} else {
|
||||||
|
// fallback for unknown object types
|
||||||
|
targetType = "user";
|
||||||
|
if (bite.to !== undefined) {
|
||||||
|
const to = Array.isArray(bite.to) ? bite.to[0] : bite.to;
|
||||||
|
targetId = (to as string).split("/").pop();
|
||||||
|
} else {
|
||||||
|
const biteTarget = await resolver.resolve(bite.target);
|
||||||
|
const targetActor =
|
||||||
|
(biteTarget as IActivity).actor || biteTarget.attributedTo;
|
||||||
|
const targetActorId =
|
||||||
|
typeof targetActor === "string" ? targetActor : (targetActor as any).id;
|
||||||
|
if (!targetActorId.startsWith(`${config.url}/`)) {
|
||||||
|
return "skip: indirect target is not local";
|
||||||
|
}
|
||||||
|
targetId = targetActorId.split("/").pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await createBite(
|
||||||
|
biteActor,
|
||||||
|
targetType,
|
||||||
|
targetId,
|
||||||
|
bite.id!,
|
||||||
|
bite.published ? new Date(bite.published) : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return "ok";
|
||||||
|
};
|
|
@ -19,6 +19,7 @@ import {
|
||||||
isFlag,
|
isFlag,
|
||||||
isMove,
|
isMove,
|
||||||
getApId,
|
getApId,
|
||||||
|
isBite,
|
||||||
} from "../type.js";
|
} from "../type.js";
|
||||||
import { apLogger } from "../logger.js";
|
import { apLogger } from "../logger.js";
|
||||||
import Resolver from "../resolver.js";
|
import Resolver from "../resolver.js";
|
||||||
|
@ -37,6 +38,7 @@ 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";
|
import move from "./move/index.js";
|
||||||
|
import bite from "./bite.js";
|
||||||
import type { IObject } from "../type.js";
|
import type { IObject } from "../type.js";
|
||||||
import { extractDbHost } from "@/misc/convert-host.js";
|
import { extractDbHost } from "@/misc/convert-host.js";
|
||||||
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
||||||
|
@ -105,6 +107,8 @@ async function performOneActivity(
|
||||||
await flag(actor, activity);
|
await flag(actor, activity);
|
||||||
} else if (isMove(activity)) {
|
} else if (isMove(activity)) {
|
||||||
await move(actor, activity);
|
await move(actor, activity);
|
||||||
|
} else if (isBite(activity)) {
|
||||||
|
await bite(actor, activity);
|
||||||
} else {
|
} else {
|
||||||
apLogger.warn(`unrecognized activity type: ${(activity as any).type}`);
|
apLogger.warn(`unrecognized activity type: ${(activity as any).type}`);
|
||||||
}
|
}
|
||||||
|
|
12
packages/backend/src/remote/activitypub/renderer/bite.ts
Normal file
12
packages/backend/src/remote/activitypub/renderer/bite.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import config from "@/config/index.js";
|
||||||
|
import { Bites } from "@/models/index.js";
|
||||||
|
import { Bite } from "@/models/entities/bite.js";
|
||||||
|
|
||||||
|
export default async (bite: Bite) => ({
|
||||||
|
id: `${config.url}/bites/${bite.id}`,
|
||||||
|
type: "Bite",
|
||||||
|
actor: `${config.url}/users/${bite.userId}`,
|
||||||
|
target: await Bites.targetUri(bite),
|
||||||
|
published: bite.createdAt.toISOString(),
|
||||||
|
to: await Bites.targetUserId(bite),
|
||||||
|
});
|
|
@ -47,6 +47,8 @@ export const renderActivity = (x: any): IActivity | null => {
|
||||||
fedibird: "http://fedibird.com/ns#",
|
fedibird: "http://fedibird.com/ns#",
|
||||||
// vcard
|
// vcard
|
||||||
vcard: "http://www.w3.org/2006/vcard/ns#",
|
vcard: "http://www.w3.org/2006/vcard/ns#",
|
||||||
|
// mia
|
||||||
|
Bite: "https://ns.mia.jetzt/as#Bite",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
NoteReactions,
|
NoteReactions,
|
||||||
Polls,
|
Polls,
|
||||||
Users,
|
Users,
|
||||||
|
Bites,
|
||||||
} from "@/models/index.js";
|
} from "@/models/index.js";
|
||||||
import { parseUri } from "./db-resolver.js";
|
import { parseUri } from "./db-resolver.js";
|
||||||
import renderNote from "@/remote/activitypub/renderer/note.js";
|
import renderNote from "@/remote/activitypub/renderer/note.js";
|
||||||
|
@ -25,6 +26,7 @@ import renderFollow from "@/remote/activitypub/renderer/follow.js";
|
||||||
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
import { shouldBlockInstance } from "@/misc/should-block-instance.js";
|
||||||
import { apLogger } from "@/remote/activitypub/logger.js";
|
import { apLogger } from "@/remote/activitypub/logger.js";
|
||||||
import { In, IsNull, Not } from "typeorm";
|
import { In, IsNull, Not } from "typeorm";
|
||||||
|
import renderBite from "@/remote/activitypub/renderer/bite.js";
|
||||||
|
|
||||||
export default class Resolver {
|
export default class Resolver {
|
||||||
private history: Set<string>;
|
private history: Set<string>;
|
||||||
|
@ -219,6 +221,10 @@ export default class Resolver {
|
||||||
throw new Error("resolveLocal: invalid follow URI");
|
throw new Error("resolveLocal: invalid follow URI");
|
||||||
}
|
}
|
||||||
return renderActivity(renderFollow(follower, followee, url));
|
return renderActivity(renderFollow(follower, followee, url));
|
||||||
|
case "bites":
|
||||||
|
return Bites.findOneByOrFail({ id: parsed.id }).then((bite) =>
|
||||||
|
renderActivity(renderBite(bite)),
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
throw new Error(`resolveLocal: type ${type} unhandled`);
|
throw new Error(`resolveLocal: type ${type} unhandled`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -322,6 +322,12 @@ export interface IMove extends IActivity {
|
||||||
target: IObject | string;
|
target: IObject | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IBite extends IActivity {
|
||||||
|
type: "Bite";
|
||||||
|
actor: string;
|
||||||
|
target: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const isCreate = (object: IObject): object is ICreate =>
|
export const isCreate = (object: IObject): object is ICreate =>
|
||||||
getApType(object) === "Create";
|
getApType(object) === "Create";
|
||||||
export const isDelete = (object: IObject): object is IDelete =>
|
export const isDelete = (object: IObject): object is IDelete =>
|
||||||
|
@ -354,3 +360,5 @@ export const isFlag = (object: IObject): object is IFlag =>
|
||||||
getApType(object) === "Flag";
|
getApType(object) === "Flag";
|
||||||
export const isMove = (object: IObject): object is IMove =>
|
export const isMove = (object: IObject): object is IMove =>
|
||||||
getApType(object) === "Move";
|
getApType(object) === "Move";
|
||||||
|
export const isBite = (object: IObject): object is IBite =>
|
||||||
|
getApType(object) === "Bite";
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
Emojis,
|
Emojis,
|
||||||
NoteReactions,
|
NoteReactions,
|
||||||
FollowRequests,
|
FollowRequests,
|
||||||
|
Bites,
|
||||||
} from "@/models/index.js";
|
} from "@/models/index.js";
|
||||||
import type { ILocalUser, User } from "@/models/entities/user.js";
|
import type { ILocalUser, User } from "@/models/entities/user.js";
|
||||||
import { renderLike } from "@/remote/activitypub/renderer/like.js";
|
import { renderLike } from "@/remote/activitypub/renderer/like.js";
|
||||||
|
@ -35,6 +36,7 @@ import Outbox, { packActivity } from "./activitypub/outbox.js";
|
||||||
import { serverLogger } from "./index.js";
|
import { serverLogger } from "./index.js";
|
||||||
import config from "@/config/index.js";
|
import config from "@/config/index.js";
|
||||||
import Koa from "koa";
|
import Koa from "koa";
|
||||||
|
import renderBite from "@/remote/activitypub/renderer/bite.js";
|
||||||
|
|
||||||
// Init router
|
// Init router
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
@ -480,4 +482,33 @@ router.get("/follows/:followRequestId", async (ctx: Router.RouterContext) => {
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// bite
|
||||||
|
router.get("/bites/:biteId", async (ctx: Router.RouterContext) => {
|
||||||
|
const verify = await checkFetch(ctx.req);
|
||||||
|
if (verify !== 200) {
|
||||||
|
ctx.status = verify;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bite = await Bites.findOne({
|
||||||
|
where: { id: ctx.params.biteId },
|
||||||
|
relations: ["targetUser", "targetBite"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bite === null) {
|
||||||
|
ctx.status = 404;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
if (meta.secureMode || meta.privateMode) {
|
||||||
|
ctx.set("Cache-Control", "private, max-age=0, must-revalidate");
|
||||||
|
} else {
|
||||||
|
ctx.set("Cache-Control", "public, max-age=180");
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.body = renderActivity(await renderBite(bite));
|
||||||
|
setResponseType(ctx);
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -334,6 +334,8 @@ import * as ep___users_show from "./endpoints/users/show.js";
|
||||||
import * as ep___users_stats from "./endpoints/users/stats.js";
|
import * as ep___users_stats from "./endpoints/users/stats.js";
|
||||||
import * as ep___fetchRss from "./endpoints/fetch-rss.js";
|
import * as ep___fetchRss from "./endpoints/fetch-rss.js";
|
||||||
import * as ep___admin_driveCapOverride from "./endpoints/admin/drive-capacity-override.js";
|
import * as ep___admin_driveCapOverride from "./endpoints/admin/drive-capacity-override.js";
|
||||||
|
import * as ep___bites_create from "./endpoints/bites/create.js";
|
||||||
|
import * as ep___bites_show from "./endpoints/bites/show.js";
|
||||||
|
|
||||||
//Iceshrimp Move
|
//Iceshrimp Move
|
||||||
import * as ep___i_move from "./endpoints/i/move.js";
|
import * as ep___i_move from "./endpoints/i/move.js";
|
||||||
|
@ -682,6 +684,8 @@ const eps = [
|
||||||
["admin/drive-capacity-override", ep___admin_driveCapOverride],
|
["admin/drive-capacity-override", ep___admin_driveCapOverride],
|
||||||
["fetch-rss", ep___fetchRss],
|
["fetch-rss", ep___fetchRss],
|
||||||
["get-sounds", ep___sounds],
|
["get-sounds", ep___sounds],
|
||||||
|
["bites/create", ep___bites_create],
|
||||||
|
["bites/show", ep___bites_show],
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface IEndpointMeta {
|
export interface IEndpointMeta {
|
||||||
|
|
37
packages/backend/src/server/api/endpoints/bites/create.ts
Normal file
37
packages/backend/src/server/api/endpoints/bites/create.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { Bites } from "@/models/index.js";
|
||||||
|
import define from "../../define.js";
|
||||||
|
import { createBite } from "@/services/create-bite.js";
|
||||||
|
import { MINUTE } from "@/const.js";
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ["bites"],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
limit: {
|
||||||
|
duration: MINUTE,
|
||||||
|
max: 30,
|
||||||
|
},
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: "object",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
ref: "Bite",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
targetType: { type: "string", enum: ["user", "bite"] },
|
||||||
|
targetId: { type: "string", format: "misskey:id" },
|
||||||
|
},
|
||||||
|
required: ["targetType", "targetId"],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
const biteId = await createBite(user, ps.targetType, ps.targetId);
|
||||||
|
|
||||||
|
return await Bites.pack(biteId, user);
|
||||||
|
});
|
25
packages/backend/src/server/api/endpoints/bites/show.ts
Normal file
25
packages/backend/src/server/api/endpoints/bites/show.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { Bites } from "@/models/index.js";
|
||||||
|
import define from "../../define.js";
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ["bites"],
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: "object",
|
||||||
|
optional: false,
|
||||||
|
nullable: false,
|
||||||
|
ref: "Bite",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
biteId: { type: "string", format: "misskey:id" },
|
||||||
|
},
|
||||||
|
required: ["biteId"],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
return await Bites.pack(ps.biteId, user);
|
||||||
|
});
|
74
packages/backend/src/services/create-bite.ts
Normal file
74
packages/backend/src/services/create-bite.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import { genId } from "@/misc/gen-id.js";
|
||||||
|
import { Bites, Users } from "@/models/index.js";
|
||||||
|
import { Bite } from "@/models/entities/bite.js";
|
||||||
|
import { User } from "@/models/entities/user.js";
|
||||||
|
import { renderActivity } from "@/remote/activitypub/renderer/index.js";
|
||||||
|
import renderBite from "@/remote/activitypub/renderer/bite.js";
|
||||||
|
import { deliverToUser } from "@/remote/activitypub/deliver-manager.js";
|
||||||
|
import { createNotification } from "./create-notification.js";
|
||||||
|
|
||||||
|
export async function createBite(
|
||||||
|
sender: User,
|
||||||
|
targetType: Bite["targetType"],
|
||||||
|
targetId: string,
|
||||||
|
remoteUri: Bite["uri"] = null,
|
||||||
|
createdAt: Date | null = null,
|
||||||
|
): Promise<Bite["id"]> {
|
||||||
|
const id = genId();
|
||||||
|
|
||||||
|
const insert = {
|
||||||
|
id,
|
||||||
|
createdAt: createdAt ?? new Date(),
|
||||||
|
userId: sender.id,
|
||||||
|
targetType,
|
||||||
|
replied: false,
|
||||||
|
uri: remoteUri,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
switch (targetType) {
|
||||||
|
case "user":
|
||||||
|
insert.targetUserId = targetId;
|
||||||
|
break;
|
||||||
|
case "bite":
|
||||||
|
insert.targetBiteId = targetId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Bites.insert(insert);
|
||||||
|
|
||||||
|
const bite = await Bites.findOneOrFail({
|
||||||
|
where: { id },
|
||||||
|
relations: ["targetUser", "targetBite"],
|
||||||
|
});
|
||||||
|
|
||||||
|
let deliverTarget: User;
|
||||||
|
|
||||||
|
switch (targetType) {
|
||||||
|
case "user":
|
||||||
|
deliverTarget = bite.targetUser!;
|
||||||
|
break;
|
||||||
|
case "bite":
|
||||||
|
await Bites.update({ id: bite.targetBiteId! }, { replied: true });
|
||||||
|
deliverTarget =
|
||||||
|
bite.targetBite!.user ??
|
||||||
|
(await Users.findOneByOrFail({ id: bite.targetBite!.userId }));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Users.isLocalUser(sender) && Users.isRemoteUser(deliverTarget)) {
|
||||||
|
await deliverToUser(
|
||||||
|
sender,
|
||||||
|
renderActivity(await renderBite(bite)),
|
||||||
|
deliverTarget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Users.isLocalUser(deliverTarget)) {
|
||||||
|
await createNotification(deliverTarget.id, "bite", {
|
||||||
|
notifierId: sender.id,
|
||||||
|
biteId: bite.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
|
@ -25,7 +25,7 @@ export async function createNotification(
|
||||||
|
|
||||||
if (
|
if (
|
||||||
data.notifierId &&
|
data.notifierId &&
|
||||||
["mention", "reply", "renote", "quote", "reaction"].includes(type)
|
["mention", "reply", "renote", "quote", "reaction", "bite"].includes(type)
|
||||||
) {
|
) {
|
||||||
const notifier = await Users.findOneBy({ id: data.notifierId });
|
const notifier = await Users.findOneBy({ id: data.notifierId });
|
||||||
// suppress if the notifier does not exist or is silenced.
|
// suppress if the notifier does not exist or is silenced.
|
||||||
|
|
|
@ -11,6 +11,7 @@ export const notificationTypes = [
|
||||||
"followRequestAccepted",
|
"followRequestAccepted",
|
||||||
"groupInvited",
|
"groupInvited",
|
||||||
"app",
|
"app",
|
||||||
|
"bite",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const noteVisibilities = [
|
export const noteVisibilities = [
|
||||||
|
|
124
packages/client/src/components/MkBiteButton.vue
Normal file
124
packages/client/src/components/MkBiteButton.vue
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
<template>
|
||||||
|
<button class="kpoogebi _button bite-button" :class="{
|
||||||
|
full,
|
||||||
|
large,
|
||||||
|
wait,
|
||||||
|
active: hasBittenBack,
|
||||||
|
}" :disabled="wait" @click.stop="onClick" :aria-label="`bite ${user.name || user.username} back`">
|
||||||
|
<span>{{ i18n.ts.biteBack }}</span><i class="ph-tooth ph-bold ph-lg"></i>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type * as Misskey from "iceshrimp-js";
|
||||||
|
import * as os from "@/os";
|
||||||
|
import { i18n } from "@/i18n";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
user: Misskey.entities.UserLite,
|
||||||
|
bite: Misskey.entities.Bite,
|
||||||
|
full: boolean,
|
||||||
|
large: boolean,
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
full: false,
|
||||||
|
large: false
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let wait = $ref(false);
|
||||||
|
let hasBittenBack = $ref<boolean>(props.bite.replied);
|
||||||
|
|
||||||
|
async function onClick() {
|
||||||
|
wait = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await os.api("bites/create", {
|
||||||
|
targetType: "bite",
|
||||||
|
targetId: props.bite.id,
|
||||||
|
});
|
||||||
|
hasBittenBack = true;
|
||||||
|
} finally {
|
||||||
|
wait = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bite-button {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--accent);
|
||||||
|
border: solid 1px var(--accent);
|
||||||
|
padding: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
border-radius: 100px;
|
||||||
|
background: var(--bg);
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
|
||||||
|
&.full {
|
||||||
|
padding: 0.2em 0.7em;
|
||||||
|
width: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.large {
|
||||||
|
font-size: 16px;
|
||||||
|
height: 38px;
|
||||||
|
padding: 0 12px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.full) {
|
||||||
|
width: 31px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
right: -5px;
|
||||||
|
bottom: -5px;
|
||||||
|
left: -5px;
|
||||||
|
border: 2px solid var(--focus);
|
||||||
|
border-radius: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--fgOnAccent);
|
||||||
|
background: var(--accent);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--accentLighten);
|
||||||
|
border-color: var(--accentLighten);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: var(--accentDarken);
|
||||||
|
border-color: var(--accentDarken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.wait {
|
||||||
|
cursor: wait !important;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
>span {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -222,6 +222,21 @@
|
||||||
:hideMenu="true"
|
:hideMenu="true"
|
||||||
/></div
|
/></div
|
||||||
></span>
|
></span>
|
||||||
|
<span
|
||||||
|
v-if="notification.type === 'bite'"
|
||||||
|
class="text"
|
||||||
|
style="opacity: 0.7">{{
|
||||||
|
notification.bite.targetType === 'user'
|
||||||
|
? i18n.ts.bitYou
|
||||||
|
: i18n.ts.bitYouBack
|
||||||
|
}}
|
||||||
|
<div v-if="full">
|
||||||
|
<MkBiteButton
|
||||||
|
:user="notification.user"
|
||||||
|
:bite="notification.bite"
|
||||||
|
:full="true"
|
||||||
|
/></div
|
||||||
|
></span>
|
||||||
<span
|
<span
|
||||||
v-if="notification.type === 'followRequestAccepted'"
|
v-if="notification.type === 'followRequestAccepted'"
|
||||||
class="text"
|
class="text"
|
||||||
|
@ -277,6 +292,7 @@ import { ref, onMounted, onUnmounted, watch } from "vue";
|
||||||
import * as misskey from "iceshrimp-js";
|
import * as misskey from "iceshrimp-js";
|
||||||
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
import XReactionIcon from "@/components/MkReactionIcon.vue";
|
||||||
import MkFollowButton from "@/components/MkFollowButton.vue";
|
import MkFollowButton from "@/components/MkFollowButton.vue";
|
||||||
|
import MkBiteButton from "@/components/MkBiteButton.vue";
|
||||||
import XReactionTooltip from "@/components/MkReactionTooltip.vue";
|
import XReactionTooltip from "@/components/MkReactionTooltip.vue";
|
||||||
import { getNoteSummary } from "@/scripts/get-note-summary";
|
import { getNoteSummary } from "@/scripts/get-note-summary";
|
||||||
import { notePage } from "@/filters/note";
|
import { notePage } from "@/filters/note";
|
||||||
|
|
|
@ -59,6 +59,13 @@ export function getUserMenu(user, router: Router = mainRouter) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function bite() {
|
||||||
|
await os.apiWithDialog("bites/create", {
|
||||||
|
targetType: "user",
|
||||||
|
targetId: user.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleMute() {
|
async function toggleMute() {
|
||||||
if (user.isMuted) {
|
if (user.isMuted) {
|
||||||
os.apiWithDialog("mute/delete", {
|
os.apiWithDialog("mute/delete", {
|
||||||
|
@ -310,6 +317,13 @@ export function getUserMenu(user, router: Router = mainRouter) {
|
||||||
action: inviteGroup,
|
action: inviteGroup,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
meId !== user.id
|
||||||
|
? {
|
||||||
|
icon: "ph-tooth ph-bold ph-lg",
|
||||||
|
text: i18n.ts.bite,
|
||||||
|
action: bite,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
icon: user.isRenoteMuted
|
icon: user.isRenoteMuted
|
||||||
|
|
|
@ -11,6 +11,7 @@ export const notificationTypes = [
|
||||||
"followRequestAccepted",
|
"followRequestAccepted",
|
||||||
"groupInvited",
|
"groupInvited",
|
||||||
"app",
|
"app",
|
||||||
|
"bite",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const noteVisibilities = [
|
export const noteVisibilities = [
|
||||||
|
|
|
@ -250,6 +250,12 @@ export type Notification = {
|
||||||
body: string;
|
body: string;
|
||||||
icon?: string | null;
|
icon?: string | null;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: "bite";
|
||||||
|
user: User;
|
||||||
|
userId: User["id"];
|
||||||
|
bite: Bite;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type MessagingMessage = {
|
export type MessagingMessage = {
|
||||||
|
@ -492,3 +498,10 @@ export type UserSorting =
|
||||||
| "+updatedAt"
|
| "+updatedAt"
|
||||||
| "-updatedAt";
|
| "-updatedAt";
|
||||||
export type OriginType = "combined" | "local" | "remote";
|
export type OriginType = "combined" | "local" | "remote";
|
||||||
|
|
||||||
|
export type Bite = {
|
||||||
|
id: ID;
|
||||||
|
user: UserLite,
|
||||||
|
targetType: "user" | "bite",
|
||||||
|
target: UserLite | Bite,
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue