Merge 8ec11827ab into 724ed47e5f
This commit is contained in:
commit
a1b121846f
|
|
@ -233,6 +233,7 @@ Meilisearchの設定に`index`が必要になりました。値はMisskeyサー
|
|||
- Node.js 18.16.0以上が必要になりました
|
||||
|
||||
### General
|
||||
- Add support for user created events. Includes basic federation of ActivityPub Event objects. [PR 10628](https://github.com/misskey-dev/misskey/pull/10628) @ssmucny
|
||||
- アカウントの引っ越し(フォロワー引き継ぎ)に対応
|
||||
- Meilisearchを全文検索に使用できるようになりました
|
||||
* 「フォロワーのみ」の投稿は検索結果に表示されません。
|
||||
|
|
|
|||
|
|
@ -1034,6 +1034,9 @@ export interface Locale {
|
|||
"accountMovedShort": string;
|
||||
"operationForbidden": string;
|
||||
"forceShowAds": string;
|
||||
"event": string;
|
||||
"events": string;
|
||||
"reverseChronological": string;
|
||||
"addMemo": string;
|
||||
"editMemo": string;
|
||||
"reactionsList": string;
|
||||
|
|
@ -1135,6 +1138,33 @@ export interface Locale {
|
|||
"_serverRules": {
|
||||
"description": string;
|
||||
};
|
||||
"_event": {
|
||||
"startDateTime": string;
|
||||
"endDateTime": string;
|
||||
"startDate": string;
|
||||
"endDate": string;
|
||||
"startTime": string;
|
||||
"endTime": string;
|
||||
"detailName": string;
|
||||
"detailValue": string;
|
||||
"location": string;
|
||||
"doorTime": string;
|
||||
"organizer": string;
|
||||
"organizerLink": string;
|
||||
"audience": string;
|
||||
"language": string;
|
||||
"ageRange": string;
|
||||
"ticketsUrl": string;
|
||||
"isFree": string;
|
||||
"price": string;
|
||||
"availability": string;
|
||||
"from": string;
|
||||
"until": string;
|
||||
"availabilityStart": string;
|
||||
"availabilityEnd": string;
|
||||
"keywords": string;
|
||||
"performers": string;
|
||||
};
|
||||
"_accountMigration": {
|
||||
"moveFrom": string;
|
||||
"moveFromSub": string;
|
||||
|
|
|
|||
|
|
@ -1031,6 +1031,9 @@ accountMoved: "このユーザーは新しいアカウントに移行しまし
|
|||
accountMovedShort: "このアカウントは移行されています"
|
||||
operationForbidden: "この操作はできません"
|
||||
forceShowAds: "常に広告を表示する"
|
||||
event: "イベント"
|
||||
events: "イベント"
|
||||
reverseChronological: "倒叙"
|
||||
addMemo: "メモを追加"
|
||||
editMemo: "メモを編集"
|
||||
reactionsList: "リアクション一覧"
|
||||
|
|
@ -1133,6 +1136,33 @@ _initialAccountSetting:
|
|||
_serverRules:
|
||||
description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
|
||||
|
||||
_event:
|
||||
startDateTime: "開始日時"
|
||||
endDateTime: "終了日時"
|
||||
startDate: "開始日"
|
||||
endDate: "終了日"
|
||||
startTime: "開始時刻"
|
||||
endTime: "終了時刻"
|
||||
detailName: "属性"
|
||||
detailValue: "値"
|
||||
location: "Location"
|
||||
doorTime: "Door Time"
|
||||
organizer: "Organizer"
|
||||
organizerLink: "Organizer Link"
|
||||
audience: "Audience"
|
||||
language: "Language"
|
||||
ageRange: "Age Range"
|
||||
ticketsUrl: "Tickets"
|
||||
isFree: "Free"
|
||||
price: "Price"
|
||||
availability: "Availability"
|
||||
from: "From"
|
||||
until: "Until"
|
||||
availabilityStart: "Availability Start"
|
||||
availabilityEnd: "Availability End"
|
||||
keywords: "Keywords"
|
||||
performers: "Performers"
|
||||
|
||||
_accountMigration:
|
||||
moveFrom: "別のアカウントからこのアカウントに移行"
|
||||
moveFromSub: "別のアカウントへエイリアスを作成"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
export class Event1681429921400 {
|
||||
name = 'Event1681429921400'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_2cd3b2a6b4cf0b910b260afe08"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_createdAt"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muteeId"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_renote_muting_muterId"`);
|
||||
await queryRunner.query(`CREATE TABLE "event" ("id" character varying(32) NOT NULL, "start" TIMESTAMP WITH TIME ZONE NOT NULL, "end" TIMESTAMP WITH TIME ZONE, "title" character varying(128) NOT NULL, "metadata" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_30c2f3bbaf6d34a55f8ae6e4614" PRIMARY KEY ("id")); COMMENT ON COLUMN "event"."start" IS 'The start time of the event'; COMMENT ON COLUMN "event"."end" IS 'The end of the event'; COMMENT ON COLUMN "event"."title" IS 'short name of event'; COMMENT ON COLUMN "event"."metadata" IS 'metadata mapping for event with more user configurable optional information'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_785ee5fc1ea38a1b9b38ff88e5" ON "event" ("start") `);
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD "eventId" character varying(32)`);
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "UQ_3af9380f266b7046cce9c992197" UNIQUE ("eventId")`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "note"."eventId" IS 'The ID of child event'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "user"."isRoot" IS 'Whether the User is the root.'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "ad"."startsAt" IS 'The expired date of the Ad.'`);
|
||||
await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "lastUsedAt" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "mascotImageUrl" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "errorImageUrl" DROP DEFAULT`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS 'The created date of the Muting.'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS 'The mutee user ID.'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS 'The muter user ID.'`);
|
||||
await queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b"`);
|
||||
await queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b"`);
|
||||
await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c"`);
|
||||
await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "UQ_e263909ca4fe5d57f8d4230dd5c"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "UQ_51cb79b5555effaf7d69ba1cff9"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "FK_10c146e4b39b443ede016f6736d"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "UQ_10c146e4b39b443ede016f6736d"`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_3fcc2c589eaefc205e0714b99c" ON "ad" ("startsAt") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_c71faf11f0a28a5c0bb506203c" ON "channel_favorite" ("userId", "channelId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_3af9380f266b7046cce9c99219" ON "note" ("eventId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_f7b9d338207e40e768e4a5265a" ON "instance" ("firstRetrievedAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_d1259a2c2b7bb413ff449e8711" ON "renote_muting" ("createdAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_7eac97594bcac5ffcf2068089b" ON "renote_muting" ("muteeId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_7aa72a5fe76019bfe8e5e0e8b7" ON "renote_muting" ("muterId") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d801c609cec4e9eb4b6b4490c" ON "renote_muting" ("muterId", "muteeId") `);
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_3af9380f266b7046cce9c992197" FOREIGN KEY ("eventId") REFERENCES "event"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6" FOREIGN KEY ("muteeId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "renote_muting" ADD CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d" FOREIGN KEY ("muterId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "FK_10c146e4b39b443ede016f6736d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "FK_10c146e4b39b443ede016f6736d"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6"`);
|
||||
await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c"`);
|
||||
await queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b"`);
|
||||
await queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7aa72a5fe76019bfe8e5e0e8b7d"`);
|
||||
await queryRunner.query(`ALTER TABLE "renote_muting" DROP CONSTRAINT "FK_7eac97594bcac5ffcf2068089b6"`);
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_3af9380f266b7046cce9c992197"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_0d801c609cec4e9eb4b6b4490c"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_7aa72a5fe76019bfe8e5e0e8b7"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_7eac97594bcac5ffcf2068089b"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_d1259a2c2b7bb413ff449e8711"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_f7b9d338207e40e768e4a5265a"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_3af9380f266b7046cce9c99219"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_c71faf11f0a28a5c0bb506203c"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_3fcc2c589eaefc205e0714b99c"`);
|
||||
await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "UQ_10c146e4b39b443ede016f6736d" UNIQUE ("userId")`);
|
||||
await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "FK_10c146e4b39b443ede016f6736d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "UQ_51cb79b5555effaf7d69ba1cff9" UNIQUE ("userId")`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "FK_51cb79b5555effaf7d69ba1cff9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6" UNIQUE ("userId")`);
|
||||
await queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "FK_f4853eb41ab722fe05f81cedeb6" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "UQ_e263909ca4fe5d57f8d4230dd5c" UNIQUE ("noteId")`);
|
||||
await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b" UNIQUE ("noteId")`);
|
||||
await queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "FK_da851e06d0dfe2ef397d8b1bf1b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muterId" IS NULL`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."muteeId" IS NULL`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "renote_muting"."createdAt" IS NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "errorImageUrl" SET DEFAULT 'https://xn--931a.moe/aiart/yubitun.png'`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "mascotImageUrl" SET DEFAULT '/assets/ai.png'`);
|
||||
await queryRunner.query(`ALTER TABLE "antenna" ALTER COLUMN "lastUsedAt" SET DEFAULT '2023-04-13 18:46:24.168209-04'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "ad"."startsAt" IS NULL`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "user"."isRoot" IS 'Whether the User is the admin.'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "note"."eventId" IS 'The ID of child event'`);
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "UQ_3af9380f266b7046cce9c992197"`);
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "eventId"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_785ee5fc1ea38a1b9b38ff88e5"`);
|
||||
await queryRunner.query(`DROP TABLE "event"`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_2cd3b2a6b4cf0b910b260afe08" ON "instance" ("firstRetrievedAt") `);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
export class Event1681673280586 {
|
||||
name = 'Event1681673280586'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_3af9380f266b7046cce9c992197"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_3af9380f266b7046cce9c99219"`);
|
||||
await queryRunner.query(`ALTER TABLE "note" RENAME COLUMN "eventId" TO "isEvent"`);
|
||||
await queryRunner.query(`ALTER TABLE "note" RENAME CONSTRAINT "UQ_3af9380f266b7046cce9c992197" TO "UQ_16484b50d1ee91555d4b8821ac3"`);
|
||||
await queryRunner.query(`ALTER TABLE "event" RENAME COLUMN "id" TO "noteId"`);
|
||||
await queryRunner.query(`ALTER TABLE "event" RENAME CONSTRAINT "PK_30c2f3bbaf6d34a55f8ae6e4614" TO "PK_2b481f231cd035e84390072bf7b"`);
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "UQ_16484b50d1ee91555d4b8821ac3"`);
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "isEvent"`);
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD "isEvent" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "event" ADD CONSTRAINT "FK_2b481f231cd035e84390072bf7b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "event" DROP CONSTRAINT "FK_2b481f231cd035e84390072bf7b"`);
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "isEvent"`);
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD "isEvent" character varying(32)`);
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "UQ_16484b50d1ee91555d4b8821ac3" UNIQUE ("isEvent")`);
|
||||
await queryRunner.query(`ALTER TABLE "event" RENAME CONSTRAINT "PK_2b481f231cd035e84390072bf7b" TO "PK_30c2f3bbaf6d34a55f8ae6e4614"`);
|
||||
await queryRunner.query(`ALTER TABLE "event" RENAME COLUMN "noteId" TO "id"`);
|
||||
await queryRunner.query(`ALTER TABLE "note" RENAME CONSTRAINT "UQ_16484b50d1ee91555d4b8821ac3" TO "UQ_3af9380f266b7046cce9c992197"`);
|
||||
await queryRunner.query(`ALTER TABLE "note" RENAME COLUMN "isEvent" TO "eventId"`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_3af9380f266b7046cce9c99219" ON "note" ("eventId") `);
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_3af9380f266b7046cce9c992197" FOREIGN KEY ("eventId") REFERENCES "event"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
export class Event1681675881633 {
|
||||
name = 'Event1681675881633'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note" RENAME COLUMN "isEvent" TO "hasEvent"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."event_notevisibility_enum" AS ENUM('public', 'home', 'followers', 'specified')`);
|
||||
await queryRunner.query(`ALTER TABLE "event" ADD "noteVisibility" "public"."event_notevisibility_enum" NOT NULL`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "event"."noteVisibility" IS '[Denormalized]'`);
|
||||
await queryRunner.query(`ALTER TABLE "event" ADD "userId" character varying(32) NOT NULL`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "event"."userId" IS '[Denormalized]'`);
|
||||
await queryRunner.query(`ALTER TABLE "event" ADD "userHost" character varying(128)`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "event"."userHost" IS '[Denormalized]'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_01cd2b829e0263917bf570cb67" ON "event" ("userId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_f6ba57dff679ccbcfe004698ec" ON "event" ("userHost") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_f6ba57dff679ccbcfe004698ec"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_01cd2b829e0263917bf570cb67"`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "event"."userHost" IS '[Denormalized]'`);
|
||||
await queryRunner.query(`ALTER TABLE "event" DROP COLUMN "userHost"`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "event"."userId" IS '[Denormalized]'`);
|
||||
await queryRunner.query(`ALTER TABLE "event" DROP COLUMN "userId"`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "event"."noteVisibility" IS '[Denormalized]'`);
|
||||
await queryRunner.query(`ALTER TABLE "event" DROP COLUMN "noteVisibility"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."event_notevisibility_enum"`);
|
||||
await queryRunner.query(`ALTER TABLE "note" RENAME COLUMN "hasEvent" TO "isEvent"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -121,6 +121,7 @@ import { ApMentionService } from './activitypub/models/ApMentionService.js';
|
|||
import { ApNoteService } from './activitypub/models/ApNoteService.js';
|
||||
import { ApPersonService } from './activitypub/models/ApPersonService.js';
|
||||
import { ApQuestionService } from './activitypub/models/ApQuestionService.js';
|
||||
import { ApEventService } from './activitypub/models/ApEventService.js';
|
||||
import { QueueModule } from './QueueModule.js';
|
||||
import { QueueService } from './QueueService.js';
|
||||
import { LoggerService } from './LoggerService.js';
|
||||
|
|
@ -248,6 +249,7 @@ const $ApMentionService: Provider = { provide: 'ApMentionService', useExisting:
|
|||
const $ApNoteService: Provider = { provide: 'ApNoteService', useExisting: ApNoteService };
|
||||
const $ApPersonService: Provider = { provide: 'ApPersonService', useExisting: ApPersonService };
|
||||
const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting: ApQuestionService };
|
||||
const $ApEventService: Provider = { provide: 'ApEventService', useExisting: ApEventService };
|
||||
//#endregion
|
||||
|
||||
@Module({
|
||||
|
|
@ -373,6 +375,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
ApNoteService,
|
||||
ApPersonService,
|
||||
ApQuestionService,
|
||||
ApEventService,
|
||||
QueueService,
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
|
|
@ -494,6 +497,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$ApNoteService,
|
||||
$ApPersonService,
|
||||
$ApQuestionService,
|
||||
$ApEventService,
|
||||
//#endregion
|
||||
],
|
||||
exports: [
|
||||
|
|
@ -615,6 +619,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
ApNoteService,
|
||||
ApPersonService,
|
||||
ApQuestionService,
|
||||
ApEventService,
|
||||
QueueService,
|
||||
|
||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||
|
|
@ -735,6 +740,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$ApNoteService,
|
||||
$ApPersonService,
|
||||
$ApQuestionService,
|
||||
$ApEventService,
|
||||
//#endregion
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ type Option = {
|
|||
renote?: MiNote | null;
|
||||
files?: MiDriveFile[] | null;
|
||||
poll?: IPoll | null;
|
||||
event?: IEvent | null;
|
||||
localOnly?: boolean | null;
|
||||
reactionAcceptance?: MiNote['reactionAcceptance'];
|
||||
cw?: string | null;
|
||||
|
|
@ -364,6 +365,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
name: data.name,
|
||||
text: data.text,
|
||||
hasPoll: data.poll != null,
|
||||
hasEvent: data.event != null,
|
||||
cw: data.cw ?? null,
|
||||
tags: tags.map(tag => normalizeForSearch(tag)),
|
||||
emojis,
|
||||
|
|
@ -408,23 +410,40 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
|
||||
// 投稿を作成
|
||||
try {
|
||||
if (insert.hasPoll) {
|
||||
if (insert.hasPoll || insert.hasEvent) {
|
||||
// Start transaction
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
await transactionalEntityManager.insert(MiNote, insert);
|
||||
|
||||
const poll = new MiPoll({
|
||||
noteId: insert.id,
|
||||
choices: data.poll!.choices,
|
||||
expiresAt: data.poll!.expiresAt,
|
||||
multiple: data.poll!.multiple,
|
||||
votes: new Array(data.poll!.choices.length).fill(0),
|
||||
noteVisibility: insert.visibility,
|
||||
userId: user.id,
|
||||
userHost: user.host,
|
||||
});
|
||||
if (insert.hasPoll) {
|
||||
const poll = new MiPoll({
|
||||
noteId: insert.id,
|
||||
choices: data.poll!.choices,
|
||||
expiresAt: data.poll!.expiresAt,
|
||||
multiple: data.poll!.multiple,
|
||||
votes: new Array(data.poll!.choices.length).fill(0),
|
||||
noteVisibility: insert.visibility,
|
||||
userId: user.id,
|
||||
userHost: user.host,
|
||||
});
|
||||
|
||||
await transactionalEntityManager.insert(MiPoll, poll);
|
||||
await transactionalEntityManager.insert(MiPoll, poll);
|
||||
}
|
||||
|
||||
if (insert.hasEvent) {
|
||||
const event = new MiEvent({
|
||||
noteId: insert.id,
|
||||
start: data.event!.start,
|
||||
end: data.event!.end ?? undefined,
|
||||
title: data.event!.title,
|
||||
metadata: data.event!.metadata,
|
||||
noteVisibility: insert.visibility,
|
||||
userId: user.id,
|
||||
userHost: user.host,
|
||||
});
|
||||
|
||||
await transactionalEntityManager.insert(MiEvent, event);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await this.notesRepository.insert(insert);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { MfmService } from '@/core/MfmService.js';
|
|||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import type { MiUserKeypair } from '@/models/entities/UserKeypair.js';
|
||||
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/index.js';
|
||||
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository, EventsRepository } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
|
|
@ -52,6 +52,9 @@ export class ApRendererService {
|
|||
@Inject(DI.pollsRepository)
|
||||
private pollsRepository: PollsRepository,
|
||||
|
||||
@Inject(DI.eventsRepository)
|
||||
private eventsRepository: EventsRepository,
|
||||
|
||||
private customEmojiService: CustomEmojiService,
|
||||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
|
|
@ -424,6 +427,18 @@ export class ApRendererService {
|
|||
})),
|
||||
} as const : {};
|
||||
|
||||
let asEvent = {};
|
||||
if (note.hasEvent) {
|
||||
const event = await this.eventsRepository.findOneBy({ noteId: note.id });
|
||||
asEvent = event ? {
|
||||
type: 'Event',
|
||||
name: event.title,
|
||||
startTime: event.start,
|
||||
endTime: event.end,
|
||||
...event.metadata,
|
||||
} as const : {};
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${this.config.url}/notes/${note.id}`,
|
||||
type: 'Note',
|
||||
|
|
@ -444,6 +459,7 @@ export class ApRendererService {
|
|||
attachment: files.map(x => this.renderDocument(x)),
|
||||
sensitive: note.cw != null || files.some(file => file.isSensitive),
|
||||
tag,
|
||||
...asEvent,
|
||||
...asPoll,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IEvent } from '@/models/entities/Event.js';
|
||||
import { isEvent } from '../type.js';
|
||||
import { ApLoggerService } from '../ApLoggerService.js';
|
||||
import { ApResolverService } from '../ApResolverService.js';
|
||||
import type { Resolver } from '../ApResolverService.js';
|
||||
import type { IObject } from '../type.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApEventService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
private apResolverService: ApResolverService,
|
||||
private apLoggerService: ApLoggerService,
|
||||
) {
|
||||
this.logger = this.apLoggerService.logger;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async extractEventFromNote(source: string | IObject, resolverParam?: Resolver): Promise<IEvent> {
|
||||
const resolver = resolverParam ?? this.apResolverService.createResolver();
|
||||
|
||||
const note = await resolver.resolve(source);
|
||||
|
||||
if (!isEvent(note)) {
|
||||
throw new Error('invalid type');
|
||||
}
|
||||
|
||||
if (note.name && note.startTime) {
|
||||
const title = note.name;
|
||||
const start = note.startTime;
|
||||
const end = note.endTime ?? null;
|
||||
|
||||
return {
|
||||
title,
|
||||
start,
|
||||
end,
|
||||
metadata: {
|
||||
'@type': 'Event',
|
||||
name: note.name,
|
||||
url: note.href,
|
||||
startDate: note.startTime.toISOString(),
|
||||
endDate: note.endTime?.toISOString(),
|
||||
description: note.summary,
|
||||
identifier: note.id,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
throw new Error('Invalid event properties');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ import { ApPersonService } from './ApPersonService.js';
|
|||
import { extractApHashtags } from './tag.js';
|
||||
import { ApMentionService } from './ApMentionService.js';
|
||||
import { ApQuestionService } from './ApQuestionService.js';
|
||||
import { ApEventService } from './ApEventService.js';
|
||||
import { ApImageService } from './ApImageService.js';
|
||||
import type { Resolver } from '../ApResolverService.js';
|
||||
import type { IObject, IPost } from '../type.js';
|
||||
|
|
@ -65,6 +66,7 @@ export class ApNoteService {
|
|||
private apMentionService: ApMentionService,
|
||||
private apImageService: ApImageService,
|
||||
private apQuestionService: ApQuestionService,
|
||||
private apEventService: ApEventService,
|
||||
private metaService: MetaService,
|
||||
private appLockService: AppLockService,
|
||||
private pollService: PollService,
|
||||
|
|
@ -270,6 +272,7 @@ export class ApNoteService {
|
|||
const apEmojis = emojis.map(emoji => emoji.name);
|
||||
|
||||
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
|
||||
const event = await this.apEventService.extractEventFromNote(note, resolver).catch(() => undefined);
|
||||
|
||||
try {
|
||||
return await this.noteCreateService.create(actor, {
|
||||
|
|
@ -287,6 +290,7 @@ export class ApNoteService {
|
|||
apHashtags,
|
||||
apEmojis,
|
||||
poll,
|
||||
event,
|
||||
uri: note.id,
|
||||
url: url,
|
||||
}, silent);
|
||||
|
|
|
|||
|
|
@ -138,6 +138,9 @@ export interface IQuestion extends IObject {
|
|||
export const isQuestion = (object: IObject): object is IQuestion =>
|
||||
getApType(object) === 'Note' || getApType(object) === 'Question';
|
||||
|
||||
export const isEvent = (object: IObject): object is IObject =>
|
||||
getApType(object) === 'Note' || getApType(object) === 'Event';
|
||||
|
||||
interface IQuestionChoice {
|
||||
name?: string;
|
||||
replies?: ICollection;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
|
|||
import type { MiUser } from '@/models/entities/User.js';
|
||||
import type { MiNote } from '@/models/entities/Note.js';
|
||||
import type { MiNoteReaction } from '@/models/entities/NoteReaction.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/index.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, EventsRepository } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
|
|
@ -45,6 +45,9 @@ export class NoteEntityService implements OnModuleInit {
|
|||
@Inject(DI.pollsRepository)
|
||||
private pollsRepository: PollsRepository,
|
||||
|
||||
@Inject(DI.eventsRepository)
|
||||
private eventsRepository: EventsRepository,
|
||||
|
||||
@Inject(DI.pollVotesRepository)
|
||||
private pollVotesRepository: PollVotesRepository,
|
||||
|
||||
|
|
@ -166,6 +169,17 @@ export class NoteEntityService implements OnModuleInit {
|
|||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async populateEvent(note: MiNote) {
|
||||
const event = await this.eventsRepository.findOneByOrFail({ noteId: note.id });
|
||||
return {
|
||||
title: event.title,
|
||||
start: event.start,
|
||||
end: event.end,
|
||||
metadata: event.metadata,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async populateMyReaction(note: MiNote, meId: MiUser['id'], _hint_?: {
|
||||
myReactions: Map<MiNote['id'], MiNoteReaction | null>;
|
||||
|
|
@ -351,6 +365,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
}) : undefined,
|
||||
|
||||
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
|
||||
event: note.hasEvent ? this.populateEvent(note) : undefined,
|
||||
|
||||
...(meId ? {
|
||||
myReaction: this.populateMyReaction(note, meId, options?._hint_),
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export const DI = {
|
|||
followRequestsRepository: Symbol('followRequestsRepository'),
|
||||
instancesRepository: Symbol('instancesRepository'),
|
||||
emojisRepository: Symbol('emojisRepository'),
|
||||
eventsRepository: Symbol('eventsRepository'),
|
||||
driveFilesRepository: Symbol('driveFilesRepository'),
|
||||
driveFoldersRepository: Symbol('driveFoldersRepository'),
|
||||
metasRepository: Symbol('metasRepository'),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAttestationChallenge, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMutedNote, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListJoining, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './index.js';
|
||||
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAttestationChallenge, MiAuthSession, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiEvent, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMutedNote, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListJoining, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook } from './index.js';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
||||
|
|
@ -171,6 +171,12 @@ const $emojisRepository: Provider = {
|
|||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $eventsRepository: Provider = {
|
||||
provide: DI.eventsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiEvent),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $driveFilesRepository: Provider = {
|
||||
provide: DI.driveFilesRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiDriveFile),
|
||||
|
|
@ -436,6 +442,7 @@ const $userMemosRepository: Provider = {
|
|||
$followRequestsRepository,
|
||||
$instancesRepository,
|
||||
$emojisRepository,
|
||||
$eventsRepository,
|
||||
$driveFilesRepository,
|
||||
$driveFoldersRepository,
|
||||
$metasRepository,
|
||||
|
|
@ -504,6 +511,7 @@ const $userMemosRepository: Provider = {
|
|||
$followRequestsRepository,
|
||||
$instancesRepository,
|
||||
$emojisRepository,
|
||||
$eventsRepository,
|
||||
$driveFilesRepository,
|
||||
$driveFoldersRepository,
|
||||
$metasRepository,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
import { Entity, Index, Column, PrimaryColumn, OneToOne, JoinColumn } from 'typeorm';
|
||||
import { id } from '../id.js';
|
||||
import { noteVisibilities } from '../../types.js';
|
||||
import { MiNote } from './Note.js';
|
||||
import type { MiUser } from './User.js';
|
||||
|
||||
@Entity('event')
|
||||
export class MiEvent {
|
||||
@PrimaryColumn(id())
|
||||
public noteId: MiNote['id'];
|
||||
|
||||
@OneToOne(type => MiNote, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public note: MiNote | null;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The start time of the event',
|
||||
})
|
||||
public start: Date;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The end of the event',
|
||||
nullable: true,
|
||||
})
|
||||
public end: Date;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 128,
|
||||
comment: 'short name of event',
|
||||
})
|
||||
public title: string;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: {
|
||||
'@context': 'https://schema.org/',
|
||||
'@type': 'Event',
|
||||
},
|
||||
comment: 'metadata object describing the event. Follows https://schema.org/Event',
|
||||
})
|
||||
public metadata: EventSchema;
|
||||
|
||||
//#region Denormalized fields
|
||||
@Column('enum', {
|
||||
enum: noteVisibilities,
|
||||
comment: '[Denormalized]',
|
||||
})
|
||||
public noteVisibility: typeof noteVisibilities[number];
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: '[Denormalized]',
|
||||
})
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
comment: '[Denormalized]',
|
||||
})
|
||||
public userHost: string | null;
|
||||
//#endregion
|
||||
|
||||
constructor(data: Partial<Event>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSchema = {
|
||||
'@type': 'Event';
|
||||
name?: string;
|
||||
url?: string;
|
||||
description?: string;
|
||||
audience?: {
|
||||
'@type': 'Audience';
|
||||
name: string;
|
||||
};
|
||||
doorTime?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
eventStatus?: 'https://schema.org/EventCancelled' | 'https://schema.org/EventMovedOnline' | 'https://schema.org/EventPostponed' | 'https://schema.org/EventRescheduled' | 'https://schema.org/EventScheduled';
|
||||
inLanguage?: string;
|
||||
isAccessibleForFree?: boolean;
|
||||
keywords?: string;
|
||||
location?: string;
|
||||
offers?: {
|
||||
'@type': 'Offer';
|
||||
price?: string;
|
||||
priceCurrency?: string;
|
||||
availabilityStarts?: string;
|
||||
availabilityEnds?: string;
|
||||
url?: string;
|
||||
};
|
||||
organizer?: {
|
||||
name: string;
|
||||
sameAs?: string; // ie. URL to website/social
|
||||
};
|
||||
performer?: {
|
||||
name: string;
|
||||
sameAs?: string; // ie. URL to website/social
|
||||
}[];
|
||||
typicalAgeRange?: string;
|
||||
identifier?: string;
|
||||
}
|
||||
|
||||
export type IEvent = {
|
||||
start: Date;
|
||||
end: Date | null
|
||||
title: string;
|
||||
metadata: EventSchema;
|
||||
}
|
||||
|
|
@ -58,6 +58,11 @@ export class MiNote {
|
|||
})
|
||||
public threadId: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public hasEvent: boolean;
|
||||
|
||||
// TODO: varcharにしたい
|
||||
@Column('text', {
|
||||
nullable: true,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { MiClipFavorite } from '@/models/entities/ClipFavorite.js';
|
|||
import { MiDriveFile } from '@/models/entities/DriveFile.js';
|
||||
import { MiDriveFolder } from '@/models/entities/DriveFolder.js';
|
||||
import { MiEmoji } from '@/models/entities/Emoji.js';
|
||||
import { MiEvent } from '@/models/entities/Event.js';
|
||||
import { MiFollowing } from '@/models/entities/Following.js';
|
||||
import { MiFollowRequest } from '@/models/entities/FollowRequest.js';
|
||||
import { MiGalleryLike } from '@/models/entities/GalleryLike.js';
|
||||
|
|
@ -90,6 +91,7 @@ export {
|
|||
MiDriveFile,
|
||||
MiDriveFolder,
|
||||
MiEmoji,
|
||||
MiEvent,
|
||||
MiFollowing,
|
||||
MiFollowRequest,
|
||||
MiGalleryLike,
|
||||
|
|
@ -158,6 +160,7 @@ export type ClipFavoritesRepository = Repository<MiClipFavorite>;
|
|||
export type DriveFilesRepository = Repository<MiDriveFile>;
|
||||
export type DriveFoldersRepository = Repository<MiDriveFolder>;
|
||||
export type EmojisRepository = Repository<MiEmoji>;
|
||||
export type EventsRepository = Repository<MiEvent>;
|
||||
export type FollowingsRepository = Repository<MiFollowing>;
|
||||
export type FollowRequestsRepository = Repository<MiFollowRequest>;
|
||||
export type GalleryLikesRepository = Repository<MiGalleryLike>;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { MiClipFavorite } from '@/models/entities/ClipFavorite.js';
|
|||
import { MiDriveFile } from '@/models/entities/DriveFile.js';
|
||||
import { MiDriveFolder } from '@/models/entities/DriveFolder.js';
|
||||
import { MiEmoji } from '@/models/entities/Emoji.js';
|
||||
import { MiEvent } from '@/models/entities/Event.js';
|
||||
import { MiFollowing } from '@/models/entities/Following.js';
|
||||
import { MiFollowRequest } from '@/models/entities/FollowRequest.js';
|
||||
import { MiGalleryLike } from '@/models/entities/GalleryLike.js';
|
||||
|
|
@ -163,6 +164,7 @@ export const entities = [
|
|||
MiPoll,
|
||||
MiPollVote,
|
||||
MiEmoji,
|
||||
MiEvent,
|
||||
MiHashtag,
|
||||
MiSwSubscription,
|
||||
MiAbuseUserReport,
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js';
|
|||
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
|
||||
import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js';
|
||||
import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js';
|
||||
import * as ep___notes_events_search from './endpoints/notes/events/search.js';
|
||||
import * as ep___notes_reactions from './endpoints/notes/reactions.js';
|
||||
import * as ep___notes_reactions_create from './endpoints/notes/reactions/create.js';
|
||||
import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete.js';
|
||||
|
|
@ -612,6 +613,7 @@ const $notes_localTimeline: Provider = { provide: 'ep:notes/local-timeline', use
|
|||
const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default };
|
||||
const $notes_polls_recommendation: Provider = { provide: 'ep:notes/polls/recommendation', useClass: ep___notes_polls_recommendation.default };
|
||||
const $notes_polls_vote: Provider = { provide: 'ep:notes/polls/vote', useClass: ep___notes_polls_vote.default };
|
||||
const $notes_events_search: Provider = { provide: 'ep:notes/events/search', useClass: ep___notes_events_search.default };
|
||||
const $notes_reactions: Provider = { provide: 'ep:notes/reactions', useClass: ep___notes_reactions.default };
|
||||
const $notes_reactions_create: Provider = { provide: 'ep:notes/reactions/create', useClass: ep___notes_reactions_create.default };
|
||||
const $notes_reactions_delete: Provider = { provide: 'ep:notes/reactions/delete', useClass: ep___notes_reactions_delete.default };
|
||||
|
|
@ -962,6 +964,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$notes_mentions,
|
||||
$notes_polls_recommendation,
|
||||
$notes_polls_vote,
|
||||
$notes_events_search,
|
||||
$notes_reactions,
|
||||
$notes_reactions_create,
|
||||
$notes_reactions_delete,
|
||||
|
|
@ -1306,6 +1309,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$notes_mentions,
|
||||
$notes_polls_recommendation,
|
||||
$notes_polls_vote,
|
||||
$notes_events_search,
|
||||
$notes_reactions,
|
||||
$notes_reactions_create,
|
||||
$notes_reactions_delete,
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js';
|
|||
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
|
||||
import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js';
|
||||
import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js';
|
||||
import * as ep___notes_events_search from './endpoints/notes/events/search.js';
|
||||
import * as ep___notes_reactions from './endpoints/notes/reactions.js';
|
||||
import * as ep___notes_reactions_create from './endpoints/notes/reactions/create.js';
|
||||
import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete.js';
|
||||
|
|
@ -610,6 +611,7 @@ const eps = [
|
|||
['notes/mentions', ep___notes_mentions],
|
||||
['notes/polls/recommendation', ep___notes_polls_recommendation],
|
||||
['notes/polls/vote', ep___notes_polls_vote],
|
||||
['notes/events/search', ep___notes_events_search],
|
||||
['notes/reactions', ep___notes_reactions],
|
||||
['notes/reactions/create', ep___notes_reactions_create],
|
||||
['notes/reactions/delete', ep___notes_reactions_delete],
|
||||
|
|
|
|||
|
|
@ -151,6 +151,16 @@ export const paramDef = {
|
|||
},
|
||||
required: ['choices'],
|
||||
},
|
||||
event: {
|
||||
type: 'object',
|
||||
nullable: true,
|
||||
properties: {
|
||||
title: { type: 'string', minLength: 1, maxLength: 128, nullable: false },
|
||||
start: { type: 'integer', nullable: false },
|
||||
end: { type: 'integer', nullable: true },
|
||||
metadata: { type: 'object' },
|
||||
},
|
||||
},
|
||||
},
|
||||
// (re)note with text, files and poll are optional
|
||||
anyOf: [
|
||||
|
|
@ -289,6 +299,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
text: ps.text ?? undefined,
|
||||
reply,
|
||||
renote,
|
||||
event: ps.event ? {
|
||||
start: new Date(ps.event.start!),
|
||||
end: ps.event.end ? new Date(ps.event.end) : null,
|
||||
title: ps.event.title!,
|
||||
metadata: ps.event.metadata ?? {},
|
||||
} : undefined,
|
||||
cw: ps.cw,
|
||||
localOnly: ps.localOnly,
|
||||
reactionAcceptance: ps.reactionAcceptance,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Event } from '@/models/entities/Event.js';
|
||||
import type { NotesRepository } from '@/models/index.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Note',
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
unavailable: {
|
||||
message: 'Search of notes unavailable.',
|
||||
code: 'UNAVAILABLE',
|
||||
id: '0b44998d-77aa-4427-80d0-d2c9b8523011',
|
||||
},
|
||||
invalidParam: {
|
||||
message: 'Invalid Parameter',
|
||||
code: 'INVALID_PARAM',
|
||||
id: 'e70903d3-0aa2-44d5-a955-4de5723c603d',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', nullable: true },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
offset: { type: 'integer', default: 0 },
|
||||
users: { type: 'array', nullable: true, items: { type: 'string', format: 'misskey:id' } },
|
||||
sinceDate: { type: 'integer', nullable: true },
|
||||
untilDate: { type: 'integer', nullable: true },
|
||||
filters: {
|
||||
type: 'array',
|
||||
nullable: true,
|
||||
description: 'list of string -> [string] that filters events based on metadata. Each item in filters is applied as an AND',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: { type: 'array', items: { type: 'string', nullable: false }, description: 'the metadata string property to filter on. Can filter on nested properties using an array. such as `["location", "postalCode"]`.' },
|
||||
values: { type: 'array', items: { type: 'string', nullable: true }, description: 'The values to match the metadata against (case insensitive regex). Each item in this array is applied as an OR. Include null to indicate match on missing metadata' },
|
||||
},
|
||||
},
|
||||
},
|
||||
sortBy: { type: 'string', nullable: true, default: 'startDate', enum: ['startDate', 'createdAt'] },
|
||||
},
|
||||
} as const;
|
||||
|
||||
function notAlphaNumeric(s: string): boolean {
|
||||
return null !== s.match(/[^\w]/);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
|
||||
if (!policies.canSearchNotes) {
|
||||
throw new ApiError(meta.errors.unavailable);
|
||||
}
|
||||
|
||||
const queryRunner = this.notesRepository.queryRunner;
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note', queryRunner), ps.sinceId, ps.untilId);
|
||||
|
||||
if (ps.users) {
|
||||
if (ps.users.length < 1) throw new ApiError(meta.errors.invalidParam);
|
||||
query.andWhere('note.userId IN (:...users)', { users: ps.users });
|
||||
}
|
||||
|
||||
query
|
||||
.innerJoinAndSelect(Event, 'event', 'event.noteId = note.id')
|
||||
.innerJoinAndSelect('note.user', 'user');
|
||||
|
||||
if (ps.query && ps.query.trim() !== '') {
|
||||
query.andWhere(new Brackets((qb) => {
|
||||
const q = (ps.query ?? '').trim();
|
||||
qb.where('event.title ILIKE :q', { q: `%${ sqlLikeEscape(q) }%` })
|
||||
.orWhere('note.text ILIKE :q', { q: `%${ sqlLikeEscape(q) }%` });
|
||||
}));
|
||||
}
|
||||
if (ps.filters) {
|
||||
const filters = ps.filters;
|
||||
|
||||
filters.forEach(f => {
|
||||
if (!f.key || !f.values) throw new ApiError(meta.errors.invalidParam);
|
||||
const filterKey = f.key;
|
||||
if (filterKey.some(notAlphaNumeric)) throw new ApiError(meta.errors.invalidParam); // schema properties don't have special characters
|
||||
const filterValues = f.values as (string | null)[];
|
||||
const matches = filterValues.filter(x => x !== null) as string[];
|
||||
const hasNull = filterValues.length !== matches.length;
|
||||
if (matches.length < 1) throw new ApiError(meta.errors.invalidParam);
|
||||
query.andWhere(new Brackets((qb) => {
|
||||
// regex match metadata values case insensitive
|
||||
qb.where('event.metadata #>> :key ~* :values', { key: `{${filterKey.join(',')}}`, values: `(${ matches.map(m => m.trim()).filter(m => m.length).join('|') })` });
|
||||
if (hasNull) {
|
||||
qb.orWhere('NOT (event.metadata ? :key)', { key: filterKey });
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
if (ps.sinceDate && ps.untilDate && ps.sinceDate > ps.untilDate) throw new ApiError(meta.errors.invalidParam);
|
||||
|
||||
if (ps.sinceDate || ps.sortBy !== 'createdAt') {
|
||||
const sinceDate = ps.sinceDate ? new Date(ps.sinceDate) : new Date();
|
||||
query.andWhere('event.start > :sinceDate', { sinceDate: sinceDate })
|
||||
.andWhere('(event.end IS NULL OR event.end > :sinceDate)', { sinceDate: sinceDate });
|
||||
}
|
||||
|
||||
if (ps.untilDate) {
|
||||
query.andWhere('event.start < :untilDate', { untilDate: new Date(ps.untilDate) });
|
||||
}
|
||||
|
||||
if (ps.sortBy === 'createdAt') {
|
||||
query.orderBy('note.createdAt', 'DESC');
|
||||
} else {
|
||||
query.orderBy('event.start', 'ASC');
|
||||
}
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||
|
||||
if (ps.offset) query.skip(ps.offset);
|
||||
|
||||
query.maxExecutionTime(250); // because we include regex expressions in where clause, defend against long running regex with timeout
|
||||
const notes = await query.take(ps.limit).getMany();
|
||||
|
||||
return await this.noteEntityService.packMany(notes, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -399,10 +399,12 @@ function toStories(component: string): Promise<string> {
|
|||
}
|
||||
|
||||
// glob('src/{components,pages,ui,widgets}/**/*.vue')
|
||||
|
||||
(async () => {
|
||||
const globs = await Promise.all([
|
||||
glob('src/components/global/*.vue'),
|
||||
glob('src/components/Mk{A,B}*.vue'),
|
||||
glob('src/components/MkEvent.vue'),
|
||||
glob('src/components/MkDigitalClock.vue'),
|
||||
glob('src/components/MkGalleryPostPreview.vue'),
|
||||
glob('src/components/MkSignupServerRules.vue'),
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ export default defineComponent({
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
getDate: {
|
||||
type: Function, // Note => date string
|
||||
required: false,
|
||||
default: undefined,
|
||||
}
|
||||
},
|
||||
|
||||
setup(props, { slots, expose }) {
|
||||
|
|
@ -63,7 +68,7 @@ export default defineComponent({
|
|||
|
||||
if (
|
||||
i !== props.items.length - 1 &&
|
||||
new Date(item.createdAt).getDate() !== new Date(props.items[i + 1].createdAt).getDate()
|
||||
new Date(getDateKey(item)).getDate() !== new Date(getDateKey(props.items[i + 1])).getDate()
|
||||
) {
|
||||
const separator = h('div', {
|
||||
class: $style['separator'],
|
||||
|
|
@ -77,12 +82,12 @@ export default defineComponent({
|
|||
h('i', {
|
||||
class: `ti ti-chevron-up ${$style['date-1-icon']}`,
|
||||
}),
|
||||
getDateText(item.createdAt),
|
||||
getDateText(getDateKey(item)),
|
||||
]),
|
||||
h('span', {
|
||||
class: $style['date-2'],
|
||||
}, [
|
||||
getDateText(props.items[i + 1].createdAt),
|
||||
getDateText(getDateKey(props.items[i + 1])),
|
||||
h('i', {
|
||||
class: `ti ti-chevron-down ${$style['date-2-icon']}`,
|
||||
}),
|
||||
|
|
@ -102,6 +107,8 @@ export default defineComponent({
|
|||
}
|
||||
});
|
||||
|
||||
const getDateKey = (item: MisskeyEntity): string => props.getDate ? props.getDate(item) : item.createdAt;
|
||||
|
||||
const renderChildren = () => {
|
||||
const children = renderChildrenImpl();
|
||||
if (isDebuggerEnabled(6864)) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import MkEvent from './MkEvent.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkEvent,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
beforeMount () {
|
||||
document.body.style.background = 'var(--panel)';
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkEvent v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
note: {
|
||||
event: {
|
||||
title: 'Come on a Tea Party!',
|
||||
start: '2017-10-25T15:00:00+0900',
|
||||
end: '2017-10-25T18:00:00+0900',
|
||||
metadata: {
|
||||
'@type': 'Event',
|
||||
location: 'Kawasaki, Japan',
|
||||
description: 'Let\'s have a tea party!',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkEvent>;
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
<template>
|
||||
<div :class="$style.container">
|
||||
<div :class="$style.title">
|
||||
<i class="ti ti-calendar-event icon"></i>
|
||||
{{ note.event!.title }}
|
||||
</div>
|
||||
<dl :class="$style.details">
|
||||
<dt :class="$style.key">{{ i18n.ts._event.startDateTime }}</dt>
|
||||
<dd :class="$style.value">
|
||||
<MkTime :time="note.event!.start" mode="detail"/>
|
||||
</dd>
|
||||
<template v-if="note.event!.end">
|
||||
<dt :class="$style.key">{{ i18n.ts._event.endDateTime }}</dt>
|
||||
<dd :class="$style.value">
|
||||
<MkTime :time="note.event!.end" mode="detail"/>
|
||||
</dd>
|
||||
</template>
|
||||
<template v-if="note.event!.metadata.doorTime">
|
||||
<dt :class="$style.key">{{ i18n.ts._event.doorTime }}</dt>
|
||||
<dd :class="$style.value">{{ note.event!.metadata.doorTime }}</dd>
|
||||
</template>
|
||||
<template v-if="note.event!.metadata.location">
|
||||
<dt :class="$style.key">{{ i18n.ts._event.location }}</dt>
|
||||
<dd :class="$style.value">{{ note.event!.metadata.location }}</dd>
|
||||
</template>
|
||||
<template v-if="note.event!.metadata.url">
|
||||
<dt :class="$style.key">{{ i18n.ts._event.url }}</dt>
|
||||
<dd :class="$style.value"><a :href="note.event!.metadata.url">{{ note.event!.metadata.url }}</a></dd>
|
||||
</template>
|
||||
<template v-if="note.event!.metadata.organizer">
|
||||
<dt :class="$style.key">{{ i18n.ts._event.organizer }}</dt>
|
||||
<dd :class="$style.value">{{ note.event!.metadata.organizer.name }}</dd>
|
||||
</template>
|
||||
<template v-if="note.event!.metadata.audience">
|
||||
<dt :class="$style.key">{{ i18n.ts._event.audience }}</dt>
|
||||
<dd :class="$style.value">{{ note.event!.metadata.audience.name }}</dd>
|
||||
</template>
|
||||
<template v-if="note.event!.metadata.inLanguage">
|
||||
<dt :class="$style.key">{{ i18n.ts._event.language }}</dt>
|
||||
<dd :class="$style.value">{{ note.event!.metadata.inLanguage }}</dd>
|
||||
</template>
|
||||
<template v-if="note.event!.metadata.typicalAgeRange">
|
||||
<dt :class="$style.key">{{ i18n.ts._event.ageRange }}</dt>
|
||||
<dd :class="$style.value">{{ note.event!.metadata.typicalAgeRange }}</dd>
|
||||
</template>
|
||||
<template v-if="note.event!.metadata.performer">
|
||||
<dt :class="$style.key">{{ i18n.ts._event.performers }}</dt>
|
||||
<dd :class="$style.value">{{ note.event!.metadata.performer.join(', ') }}</dd>
|
||||
</template>
|
||||
<template v-if="note.event!.metadata.offers?.url">
|
||||
<dt :class="$style.key">{{ i18n.ts._event.ticketsUrl }}</dt>
|
||||
<dd :class="$style.value"><a :href="note.event!.metadata.offers.url">{{ note.event!.metadata.offers.url }}</a></dd>
|
||||
</template>
|
||||
<template v-if="note.event!.metadata.isAccessibleForFree">
|
||||
<dt :class="$style.key">{{ i18n.ts._event.isFree }}</dt>
|
||||
<dd :class="$style.value">{{ i18n.ts.yes }}</dd>
|
||||
</template>
|
||||
<template v-if="note.event!.metadata.offers?.price">
|
||||
<dt :class="$style.key">{{ i18n.ts._event.price }}</dt>
|
||||
<dd :class="$style.value">{{ note.event!.metadata.offers.price }}</dd>
|
||||
</template>
|
||||
<template v-if="note.event!.metadata.offers?.availabilityStarts || note.event!.metadata.offers?.availabilityEnds">
|
||||
<dt :class="$style.key">{{ i18n.ts._event.availability }}</dt>
|
||||
<dd :class="$style.value">
|
||||
{{ [
|
||||
(note.event!.metadata.offers.availabilityStarts ? i18n.ts._event.from + note.event!.metadata.offers.availabilityStarts : ''),
|
||||
(note.event!.metadata.offers.availabilityEnds ? i18n.ts._event.until + note.event!.metadata.offers.availabilityEnds : '')]
|
||||
.join(' ') }}
|
||||
</dd>
|
||||
</template>
|
||||
<template v-if="note.event!.metadata.keywords">
|
||||
<dt :class="$style.key">{{ i18n.ts._event.keywords }}</dt>
|
||||
<dd :class="$style.value">{{ note.event!.metadata.keywords }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as misskey from 'misskey-js';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
background: var(--bg);
|
||||
border-radius: var(--radius);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.25;
|
||||
font-weight: bold;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: .5px solid var(--divider);
|
||||
}
|
||||
|
||||
.details {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-gap: 1rem;
|
||||
padding-top: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.key {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.value {
|
||||
margin: 0;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
<template>
|
||||
<div class="zmdxowut">
|
||||
<MkInput v-model="title" small type="text" class="input">
|
||||
<template #label>*{{ i18n.ts.title }}</template>
|
||||
</MkInput>
|
||||
<section>
|
||||
<div>
|
||||
<section>
|
||||
<MkInput v-model="startDate" small type="date" class="input">
|
||||
<template #label>*{{ i18n.ts._event.startDate }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<section>
|
||||
<MkInput v-model="startTime" small type="time" class="input">
|
||||
<template #label>*{{ i18n.ts._event.startTime }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<section>
|
||||
<MkInput v-model="endDate" small type="date" class="input">
|
||||
<template #label>{{ i18n.ts._event.endDate }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<section>
|
||||
<MkInput v-model="endTime" small type="time" class="input">
|
||||
<template #label>{{ i18n.ts._event.endTime }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<section>
|
||||
<MkInput v-model="location" small type="text" class="input">
|
||||
<template #label>{{ i18n.ts._event.location }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<section>
|
||||
<MkInput v-model="url" small type="url" class="input">
|
||||
<template #label>URL</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
</div>
|
||||
<div>
|
||||
<section>
|
||||
<MkSwitch v-model="showAdvanced" :disabled="false" class="input">{{ i18n.ts.advanced }}</MkSwitch>
|
||||
</section>
|
||||
</div>
|
||||
<div v-show="showAdvanced">
|
||||
<section>
|
||||
<MkInput v-model="doorTime" small type="time" class="input">
|
||||
<template #label>{{ i18n.ts._event.doorTime }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<section>
|
||||
<MkInput v-model="organizer" small type="text" class="input">
|
||||
<template #label>{{ i18n.ts._event.organizer }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<section>
|
||||
<MkInput v-model="organizerLink" small type="url" class="input">
|
||||
<template #label>{{ i18n.ts._event.organizerLink }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<section>
|
||||
<MkInput v-model="audience" small type="text" class="input">
|
||||
<template #label>{{ i18n.ts._event.audience }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<section>
|
||||
<MkInput v-model="language" small type="text" class="input">
|
||||
<template #label>{{ i18n.ts._event.language }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<section>
|
||||
<MkInput v-model="ageRange" small type="text" class="input">
|
||||
<template #label>{{ i18n.ts._event.ageRange }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<!--<section>
|
||||
<MkInput v-model="performers" small type="text" class="input">
|
||||
<template #label>{{ i18n.ts._event.performers }}</template>
|
||||
</MkInput>
|
||||
</section>-->
|
||||
<section>
|
||||
<MkInput v-model="ticketsUrl" small type="url" class="input">
|
||||
<template #label>{{ i18n.ts._event.ticketsUrl }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<section>
|
||||
<MkSwitch v-model="isFree" :disabled="false">
|
||||
{{ i18n.ts._event.isFree }}
|
||||
</MkSwitch>
|
||||
</section>
|
||||
<section>
|
||||
<MkInput v-model="price" small type="text" class="input">
|
||||
<template #label>{{ i18n.ts._event.price }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<section>
|
||||
<MkInput v-model="availabilityStart" small type="datetime-local" class="input">
|
||||
<template #label>{{ i18n.ts._event.availabilityStart }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<section>
|
||||
<MkInput v-model="availabilityEnd" small type="datetime-local" class="input">
|
||||
<template #label>{{ i18n.ts._event.availabilityEnd }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<section>
|
||||
<MkInput v-model="keywords" small type="text" class="input">
|
||||
<template #label>{{ i18n.ts._event.keywords }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as misskey from 'misskey-js';
|
||||
import { Ref, ref, watch } from 'vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkSwitch from './MkSwitch.vue';
|
||||
import { formatDateTimeString } from '@/scripts/format-time-string';
|
||||
import { addTime } from '@/scripts/time';
|
||||
import { i18n } from '@/i18n';
|
||||
import date from '@/filters/date';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: misskey.entities.Note['event']
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', v: {
|
||||
model: misskey.entities.Note['event']
|
||||
})
|
||||
}>();
|
||||
|
||||
const title = ref(props.modelValue?.title ?? null);
|
||||
const startDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
|
||||
const startTime = ref('00:00');
|
||||
const endDate = ref('');
|
||||
const endTime = ref('');
|
||||
const location = ref(props.modelValue?.metadata.location ?? null);
|
||||
const url = ref(props.modelValue?.metadata.url ?? null);
|
||||
const showAdvanced = ref(false);
|
||||
const doorTime = ref(props.modelValue?.metadata.doorTime ?? null);
|
||||
const organizer = ref(props.modelValue?.metadata.organizer?.name ?? null);
|
||||
const organizerLink = ref(props.modelValue?.metadata.organizer?.sameAs ?? null);
|
||||
const audience = ref(props.modelValue?.metadata.audience?.name ?? null);
|
||||
const language = ref(props.modelValue?.metadata.inLanguage ?? null);
|
||||
const ageRange = ref(props.modelValue?.metadata.typicalAgeRange ?? null);
|
||||
const ticketsUrl = ref(props.modelValue?.metadata.offers?.url ?? null);
|
||||
const isFree = ref(props.modelValue?.metadata.isAccessibleForFree ?? false);
|
||||
const price = ref(props.modelValue?.metadata.offers?.price ?? null);
|
||||
const availabilityStart = ref(props.modelValue?.metadata.offers?.availabilityStarts ?? null);
|
||||
const availabilityEnd = ref(props.modelValue?.metadata.offers?.availabilityEnds ?? null);
|
||||
const keywords = ref(props.modelValue?.metadata.keywords ?? null);
|
||||
|
||||
function get(): misskey.entities.Note['event'] {
|
||||
const calcAt = (date: Ref<string>, time: Ref<string>): number => (new Date(`${date.value} ${time.value}`)).getTime();
|
||||
|
||||
const start = calcAt(startDate, startTime);
|
||||
const end = endDate.value ? calcAt(endDate, endTime) : null;
|
||||
return {
|
||||
title: title.value,
|
||||
start: start,
|
||||
end: end,
|
||||
metadata: {
|
||||
'@type': 'Event',
|
||||
name: title.value,
|
||||
startDate: (new Date(start)).toISOString(),
|
||||
endDate: end ? (new Date(end)).toISOString() : undefined,
|
||||
location: location.value ?? undefined,
|
||||
url: url.value ?? undefined,
|
||||
doorTime: doorTime.value ?? undefined,
|
||||
organizer: organizer.value ? {
|
||||
'@type': 'Thing',
|
||||
name: organizer.value,
|
||||
sameAs: organizerLink.value ?? undefined,
|
||||
} : undefined,
|
||||
audience: audience.value ? {
|
||||
'@type': 'Audience',
|
||||
name: audience.value,
|
||||
} : undefined,
|
||||
inLanguage: language.value ?? undefined,
|
||||
typicalAgeRange: ageRange.value ?? undefined,
|
||||
isAccessibleForFree: isFree,
|
||||
offers: ticketsUrl.value || price.value ? {
|
||||
price: price.value ?? undefined,
|
||||
priceCurrency: undefined,
|
||||
availabilityStarts: availabilityStart.value ?? undefined,
|
||||
availabilityEnds: availabilityEnd.value ?? undefined,
|
||||
url: ticketsUrl.value ?? undefined,
|
||||
} : undefined,
|
||||
keywords: keywords.value ?? undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
watch([title, startDate, startTime, endDate, endTime, location, url, doorTime, organizer, organizerLink, audience, language,
|
||||
ageRange, ticketsUrl, isFree, price, availabilityStart, availabilityEnd, keywords], () => emit('update:modelValue', get()), {
|
||||
deep: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.zmdxowut {
|
||||
padding: 8px 16px;
|
||||
|
||||
>section {
|
||||
margin: 16px 0 0 0;
|
||||
|
||||
>div {
|
||||
margin: 0 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
&:last-child {
|
||||
flex: 1 0 auto;
|
||||
|
||||
>div {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
>section {
|
||||
// MAGIC: Prevent div above from growing unless wrapped to its own line
|
||||
flex-grow: 9999;
|
||||
align-items: end;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
>.input {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -52,6 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkNoteHeader :note="appearNote" :mini="true"/>
|
||||
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
|
||||
<div style="container-type: inline-size;">
|
||||
<MkEvent v-if="appearNote.event" :note="appearNote"/>
|
||||
<p v-if="appearNote.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :i="$i"/>
|
||||
<MkCwButton v-model="showContent" :note="appearNote"/>
|
||||
|
|
@ -151,6 +152,7 @@ import MkPoll from '@/components/MkPoll.vue';
|
|||
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||
import MkEvent from '@/components/MkEvent.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
||||
import { checkWordMute } from '@/scripts/check-word-mute';
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
ref="notes"
|
||||
v-slot="{ item: note }"
|
||||
:items="notes"
|
||||
:getDate="getDate"
|
||||
:direction="pagination.reversed ? 'up' : 'down'"
|
||||
:reversed="pagination.reversed"
|
||||
:noGap="noGap"
|
||||
|
|
@ -42,6 +43,7 @@ import { infoImageUrl } from '@/instance';
|
|||
const props = defineProps<{
|
||||
pagination: Paging;
|
||||
noGap?: boolean;
|
||||
getDate?: (any) => string; // custom function to separate notes on something that isn't createdAt
|
||||
}>();
|
||||
|
||||
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkNoteSimple v-if="reply" :class="$style.targetNote" :note="reply"/>
|
||||
<MkNoteSimple v-if="renote" :class="$style.targetNote" :note="renote"/>
|
||||
<div v-if="quoteId" :class="$style.withQuote"><i class="ti ti-quote"></i> {{ i18n.ts.quoteAttached }}<button @click="quoteId = null"><i class="ti ti-x"></i></button></div>
|
||||
<MkEventEditor v-if="event" v-model="event" @destroyed="event = null"/>
|
||||
<div v-if="visibility === 'specified'" :class="$style.toSpecified">
|
||||
<span style="margin-right: 8px;">{{ i18n.ts.recipient }}</span>
|
||||
<div :class="$style.visibleUsers">
|
||||
|
|
@ -80,6 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.footerLeft">
|
||||
<button v-tooltip="i18n.ts.attachFile" class="_button" :class="$style.footerButton" @click="chooseFileFrom"><i class="ti ti-photo-plus"></i></button>
|
||||
<button v-tooltip="i18n.ts.poll" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: poll }]" @click="togglePoll"><i class="ti ti-chart-arrows"></i></button>
|
||||
<button v-tooltip="i18n.ts.event" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: event }]" @click="toggleEvent"><i class="ti ti-calendar"></i></button>
|
||||
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
|
||||
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
|
||||
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
|
||||
|
|
@ -108,6 +110,7 @@ import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
|||
import MkNotePreview from '@/components/MkNotePreview.vue';
|
||||
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
||||
import MkPollEditor from '@/components/MkPollEditor.vue';
|
||||
import MkEventEditor from '@/components/MkEventEditor.vue';
|
||||
import { host, url } from '@/config';
|
||||
import { erase, unique } from '@/scripts/array';
|
||||
import { extractMentions } from '@/scripts/extract-mentions';
|
||||
|
|
@ -170,6 +173,12 @@ let poll = $ref<{
|
|||
expiresAt: string | null;
|
||||
expiredAfter: string | null;
|
||||
} | null>(null);
|
||||
let event = $ref<{
|
||||
title: string;
|
||||
start: string;
|
||||
end: string | null;
|
||||
metadata: Record<string, string>;
|
||||
} | null>(null);
|
||||
let useCw = $ref(false);
|
||||
let showPreview = $ref(defaultStore.state.showPreview);
|
||||
watch($$(showPreview), () => defaultStore.set('showPreview', showPreview));
|
||||
|
|
@ -241,7 +250,7 @@ const maxTextLength = $computed((): number => {
|
|||
|
||||
const canPost = $computed((): boolean => {
|
||||
return !posting && !posted &&
|
||||
(1 <= textLength || 1 <= files.length || !!poll || !!props.renote) &&
|
||||
(1 <= textLength || 1 <= files.length || !!poll || !!props.renote || !!event) &&
|
||||
(textLength <= maxTextLength) &&
|
||||
(!poll || poll.choices.length >= 2);
|
||||
});
|
||||
|
|
@ -341,6 +350,7 @@ function watchForDraft() {
|
|||
watch($$(useCw), () => saveDraft());
|
||||
watch($$(cw), () => saveDraft());
|
||||
watch($$(poll), () => saveDraft());
|
||||
watch($$(event), () => saveDraft());
|
||||
watch($$(files), () => saveDraft(), { deep: true });
|
||||
watch($$(visibility), () => saveDraft());
|
||||
watch($$(localOnly), () => saveDraft());
|
||||
|
|
@ -385,6 +395,19 @@ function togglePoll() {
|
|||
}
|
||||
}
|
||||
|
||||
function toggleEvent() {
|
||||
if (event) {
|
||||
event = null;
|
||||
} else {
|
||||
event = {
|
||||
title: '',
|
||||
start: (new Date()).toString(),
|
||||
end: null,
|
||||
metadata: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function addTag(tag: string) {
|
||||
insertTextAtCursor(textareaEl, ` #${tag} `);
|
||||
}
|
||||
|
|
@ -529,6 +552,7 @@ function clear() {
|
|||
text = '';
|
||||
files = [];
|
||||
poll = null;
|
||||
event = null;
|
||||
quoteId = null;
|
||||
}
|
||||
|
||||
|
|
@ -642,6 +666,7 @@ function saveDraft() {
|
|||
localOnly: localOnly,
|
||||
files: files,
|
||||
poll: poll,
|
||||
event: event,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -703,6 +728,7 @@ async function post(ev?: MouseEvent) {
|
|||
renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined,
|
||||
channelId: props.channel ? props.channel.id : undefined,
|
||||
poll: poll,
|
||||
event: event,
|
||||
cw: useCw ? cw ?? '' : undefined,
|
||||
localOnly: localOnly,
|
||||
visibility: visibility,
|
||||
|
|
@ -870,6 +896,9 @@ onMounted(() => {
|
|||
if (draft.data.poll) {
|
||||
poll = draft.data.poll;
|
||||
}
|
||||
if (draft.data.event) {
|
||||
event = draft.data.event;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -888,6 +917,14 @@ onMounted(() => {
|
|||
expiredAfter: init.poll.expiredAfter,
|
||||
};
|
||||
}
|
||||
if (init.event) {
|
||||
event = {
|
||||
title: init.event.title,
|
||||
start: init.event.start,
|
||||
end: init.event.end,
|
||||
metadata: init.event.metadata,
|
||||
};
|
||||
}
|
||||
visibility = init.visibility;
|
||||
localOnly = init.localOnly;
|
||||
quoteId = init.renote ? init.renote.id : null;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<div class="_gaps">
|
||||
<div class="_gaps">
|
||||
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
<MkSelect v-model="eventSort" small>
|
||||
<template #label>{{ i18n.ts.sort }}</template>
|
||||
<option value="startDate">{{ i18n.ts._event.startDate }}</option>
|
||||
<option value="createdAt">{{ i18n.ts.reverseChronological }}</option>
|
||||
</MkSelect>
|
||||
<section>
|
||||
<MkInput v-model="startDate" small type="date" class="input">
|
||||
<template #label>{{ i18n.ts._event.startDate }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="endDate" small type="date" class="input">
|
||||
<template #label>{{ i18n.ts._event.endDate }}</template>
|
||||
</MkInput>
|
||||
</section>
|
||||
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
||||
</div>
|
||||
|
||||
<MkFoldableSection v-if="eventPagination">
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
<MkNotes :key="key" :pagination="eventPagination" :getDate="eventSort === 'startDate' ? note => note.event.start : undefined"/>
|
||||
</MkFoldableSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
|
||||
let searchQuery = $ref('');
|
||||
let key = $ref(0);
|
||||
let eventSort = $ref('startDate');
|
||||
let eventPagination = $ref();
|
||||
let startDate = $ref(null);
|
||||
let endDate = $ref(null);
|
||||
|
||||
async function search(): Promise<void> {
|
||||
const query = searchQuery.toString().trim();
|
||||
|
||||
// only notes/users search require the query string
|
||||
if (query == null || query === '') return;
|
||||
|
||||
eventPagination = {
|
||||
endpoint: 'notes/events/search',
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
params: {
|
||||
query: !searchQuery ? undefined : searchQuery,
|
||||
sortBy: eventSort,
|
||||
sinceDate: startDate ? (new Date(startDate)).getTime() : undefined,
|
||||
untilDate: endDate ? (new Date(endDate)).getTime() + 1000 * 3600 * 24 : undefined,
|
||||
},
|
||||
};
|
||||
|
||||
key++;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -19,20 +19,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSpacer v-else-if="tab === 'user'" :contentMax="800">
|
||||
<XUser/>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'event'" :contentMax="800">
|
||||
<XEvent/>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, onMounted } from 'vue';
|
||||
import { computed, defineAsyncComponent } from 'vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
import { instance } from '@/instance';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { $ref } from 'vue/macros';
|
||||
|
||||
const XNote = defineAsyncComponent(() => import('./search.note.vue'));
|
||||
const XUser = defineAsyncComponent(() => import('./search.user.vue'));
|
||||
const XEvent = defineAsyncComponent(() => import('./search.event.vue'));
|
||||
|
||||
let tab = $ref('note');
|
||||
|
||||
|
|
@ -48,6 +52,10 @@ const headerTabs = $computed(() => [{
|
|||
key: 'user',
|
||||
title: i18n.ts.users,
|
||||
icon: 'ti ti-users',
|
||||
}, {
|
||||
key: 'event',
|
||||
title: i18n.ts.events,
|
||||
icon: 'ti ti-calendar',
|
||||
}]);
|
||||
|
||||
definePageMetadata(computed(() => ({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<MkSpacer :contentMax="800" style="padding-top: 0">
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<MkTab v-model="include" :class="$style.tab">
|
||||
<option value="upcoming">{{ i18n.ts._event.startDate }}</option>
|
||||
<option :value="null">{{ i18n.ts.reverseChronological }}</option>
|
||||
</MkTab>
|
||||
</template>
|
||||
<MkNotes :noGap="true" :pagination="pagination" :class="$style.tl" :getDate="include === 'upcoming' ? note => note.event.start : undefined "/>
|
||||
</MkStickyContainer>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
user: misskey.entities.UserDetailed;
|
||||
}>();
|
||||
|
||||
const include = ref<string | null>('upcoming');
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'notes/events/search' as const,
|
||||
limit: 10,
|
||||
offsetMode: include.value === 'upcoming',
|
||||
params: computed(() => ({
|
||||
users: [props.user.id],
|
||||
sortBy: include.value === 'upcoming' ? 'startDate' : 'createdAt',
|
||||
})),
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.tab {
|
||||
margin: calc(var(--margin) / 2) 0;
|
||||
padding: calc(var(--margin) / 2) 0;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.tl {
|
||||
background: var(--bg);
|
||||
border-radius: var(--radius);
|
||||
overflow: clip;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="user">
|
||||
<XHome v-if="tab === 'home'" :user="user"/>
|
||||
<XTimeline v-else-if="tab === 'notes'" :user="user"/>
|
||||
<XEvent v-else-if="tab === 'events'" :user="user"/>
|
||||
<XActivity v-else-if="tab === 'activity'" :user="user"/>
|
||||
<XAchievements v-else-if="tab === 'achievements'" :user="user"/>
|
||||
<XReactions v-else-if="tab === 'reactions'" :user="user"/>
|
||||
|
|
@ -37,6 +38,7 @@ import { $i } from '@/account';
|
|||
|
||||
const XHome = defineAsyncComponent(() => import('./home.vue'));
|
||||
const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
|
||||
const XEvent = defineAsyncComponent(() => import('./events.vue'));
|
||||
const XActivity = defineAsyncComponent(() => import('./activity.vue'));
|
||||
const XAchievements = defineAsyncComponent(() => import('./achievements.vue'));
|
||||
const XReactions = defineAsyncComponent(() => import('./reactions.vue'));
|
||||
|
|
@ -81,6 +83,10 @@ const headerTabs = $computed(() => user ? [{
|
|||
key: 'notes',
|
||||
title: i18n.ts.notes,
|
||||
icon: 'ti ti-pencil',
|
||||
}, {
|
||||
key: 'events',
|
||||
title: i18n.ts.events,
|
||||
icon: 'ti ti-calendar',
|
||||
}, {
|
||||
key: 'activity',
|
||||
title: i18n.ts.activity,
|
||||
|
|
|
|||
|
|
@ -1727,6 +1727,12 @@ export type Endpoints = {
|
|||
expiresAt?: null | number;
|
||||
expiredAfter?: null | number;
|
||||
};
|
||||
event?: null | {
|
||||
title: string;
|
||||
start: number;
|
||||
end?: null | number;
|
||||
metadata: Record<string, string>;
|
||||
};
|
||||
};
|
||||
res: {
|
||||
createdNote: Note;
|
||||
|
|
@ -1804,6 +1810,24 @@ export type Endpoints = {
|
|||
};
|
||||
res: null;
|
||||
};
|
||||
'notes/events/search': {
|
||||
req: {
|
||||
query?: string;
|
||||
sinceId?: Note['id'];
|
||||
untilId?: Note['id'];
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
users?: User['id'][];
|
||||
sinceDate?: number;
|
||||
untilDate?: number;
|
||||
sortBy?: 'startDate' | 'craetedAt';
|
||||
filters?: {
|
||||
key: string[];
|
||||
values: (string | null)[];
|
||||
}[];
|
||||
};
|
||||
res: Note[];
|
||||
};
|
||||
'notes/reactions': {
|
||||
req: {
|
||||
noteId: Note['id'];
|
||||
|
|
@ -2514,6 +2538,12 @@ type Note = {
|
|||
replyId: Note['id'];
|
||||
renote?: Note;
|
||||
renoteId: Note['id'];
|
||||
event?: {
|
||||
title: string;
|
||||
start: DateString;
|
||||
end: DateString | null;
|
||||
metadata: Record<string, string>;
|
||||
};
|
||||
files: DriveFile[];
|
||||
fileIds: DriveFile['id'][];
|
||||
visibility: 'public' | 'home' | 'followers' | 'specified';
|
||||
|
|
|
|||
|
|
@ -506,6 +506,12 @@ export type Endpoints = {
|
|||
expiresAt?: null | number;
|
||||
expiredAfter?: null | number;
|
||||
};
|
||||
event?: null | {
|
||||
title: string;
|
||||
start: number;
|
||||
end?: null | number;
|
||||
metadata: Record<string, string>;
|
||||
}
|
||||
}; res: { createdNote: Note }; };
|
||||
'notes/delete': { req: { noteId: Note['id']; }; res: null; };
|
||||
'notes/favorites/create': { req: { noteId: Note['id']; }; res: null; };
|
||||
|
|
@ -517,6 +523,18 @@ export type Endpoints = {
|
|||
'notes/mentions': { req: { following?: boolean; limit?: number; sinceId?: Note['id']; untilId?: Note['id']; }; res: Note[]; };
|
||||
'notes/polls/recommendation': { req: TODO; res: TODO; };
|
||||
'notes/polls/vote': { req: { noteId: Note['id']; choice: number; }; res: null; };
|
||||
'notes/events/search': { req: {
|
||||
query?: string;
|
||||
sinceId?: Note['id'];
|
||||
untilId?: Note['id'];
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
users?: User['id'][];
|
||||
sinceDate?: number;
|
||||
untilDate?: number;
|
||||
sortBy?: 'startDate' | 'craetedAt';
|
||||
filters?: { key: string[], values: (string | null)[] }[];
|
||||
}; res: Note[]; };
|
||||
'notes/reactions': { req: { noteId: Note['id']; type?: string | null; limit?: number; }; res: NoteReaction[]; };
|
||||
'notes/reactions/create': { req: { noteId: Note['id']; reaction: string; }; res: null; };
|
||||
'notes/reactions/delete': { req: { noteId: Note['id']; }; res: null; };
|
||||
|
|
|
|||
|
|
@ -166,6 +166,12 @@ export type Note = {
|
|||
replyId: Note['id'];
|
||||
renote?: Note;
|
||||
renoteId: Note['id'];
|
||||
event?: {
|
||||
title: string,
|
||||
start: DateString,
|
||||
end: DateString | null,
|
||||
metadata: Record<string, string>,
|
||||
};
|
||||
files: DriveFile[];
|
||||
fileIds: DriveFile['id'][];
|
||||
visibility: 'public' | 'home' | 'followers' | 'specified';
|
||||
|
|
|
|||
Loading…
Reference in New Issue