diff --git a/assets/openapi.json b/assets/openapi.json index ee3b87461..b895e6a17 100644 Binary files a/assets/openapi.json and b/assets/openapi.json differ diff --git a/assets/schemas.json b/assets/schemas.json index 289dec600..5007befbd 100644 Binary files a/assets/schemas.json and b/assets/schemas.json differ diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index d19b73ad8..f78827edf 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -51,7 +51,7 @@ import { } from "@spacebar/util"; import { HTTPError } from "lambert-server"; import { In, Or, Equal, IsNull } from "typeorm"; -import { ChannelType, Embed, EmbedType, MessageCreateAttachment, MessageCreateCloudAttachment, MessageCreateSchema, MessageType, Reaction } from "@spacebar/schemas"; +import { ChannelType, Embed, EmbedType, MessageCreateAttachment, MessageCreateCloudAttachment, MessageCreateSchema, MessageType, Reaction, ReadStateType } from "@spacebar/schemas"; const allow_empty = false; // TODO: check webhook, application, system author, stickers // TODO: embed gifs/videos/images @@ -444,7 +444,7 @@ export async function handleMessage(opts: MessageOptions): Promise { await fillInMissingIDs((await Member.find({ where: { guild_id: channel.guild_id } })).map(({ id }) => id)); } const repository = ReadState.getRepository(); - const condition = { channel_id: channel.id }; + const condition = { channel_id: channel.id, type: ReadStateType.CHANNEL }; await repository.update({ ...condition, mention_count: IsNull() }, { mention_count: 0 }); await repository.increment(condition, "mention_count", 1); } else { @@ -467,11 +467,9 @@ export async function handleMessage(opts: MessageOptions): Promise { } if (users.size) { const repository = ReadState.getRepository(); - const condition = { user_id: Or(...[...users].map((id) => Equal(id))), channel_id: channel.id }; + const condition = { user_id: Or(...[...users].map((id) => Equal(id))), channel_id: channel.id, read_state_type: ReadStateType.CHANNEL }; await fillInMissingIDs([...users]); - - await repository.update({ ...condition, mention_count: IsNull() }, { mention_count: 0 }); await repository.increment(condition, "mention_count", 1); } } diff --git a/src/util/entities/ReadState.ts b/src/util/entities/ReadState.ts index e334cd73c..b4638fdf5 100644 --- a/src/util/entities/ReadState.ts +++ b/src/util/entities/ReadState.ts @@ -20,6 +20,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeor import { BaseClass } from "./BaseClass"; import { Channel } from "./Channel"; import { User } from "./User"; +import { ReadStateFlags, ReadStateType } from "@spacebar/schemas"; // for read receipts // notification cursor and public read receipt need to be forwards-only (the former to prevent re-pinging when marked as unread, and the latter to be acceptable as a legal acknowledgement in criminal proceedings), and private read marker needs to be advance-rewind capable @@ -50,25 +51,43 @@ export class ReadState extends BaseClass { }) user: User; - // fully read marker @Column({ nullable: true }) - last_message_id: string; + last_message_id?: string; - // public read receipt @Column({ nullable: true }) - public_ack: string; + last_acked_id?: string; - // notification cursor / private read receipt @Column({ nullable: true }) notifications_cursor: string; + @Column({ default: 0 }) + mention_count: number; + + @Column({ default: 0 }) + badge_count: number; + @Column({ nullable: true }) last_pin_timestamp?: Date; - @Column({ nullable: true }) - mention_count: number; + @Column({ default: ReadStateType.CHANNEL }) + read_state_type: ReadStateType; - // @Column({ nullable: true }) - // TODO: derive this from (last_message_id=notifications_cursor=public_ack)=true - manual: boolean; + @Column({ default: 0 }) + flags: ReadStateFlags; + + toJSON() { + const res = { ...this } as Partial; + if (this.read_state_type === ReadStateType.CHANNEL) { + delete res.badge_count; + delete res.last_acked_id; + } else { + delete res.mention_count; // mutually exclusive with badge_count + delete res.last_message_id; // mutually exclusive with last_acked_id + // these only apply to channels: + delete res.last_pin_timestamp; + delete res.flags; + // delete res.last_viewed; // TODO + } + return res; + } } diff --git a/src/util/migration/postgres/1771825341528-MoreReadStateFields.ts b/src/util/migration/postgres/1771825341528-MoreReadStateFields.ts new file mode 100644 index 000000000..3b0d05af1 --- /dev/null +++ b/src/util/migration/postgres/1771825341528-MoreReadStateFields.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class MoreReadStateFields1771825341528 implements MigrationInterface { + name = "MoreReadStateFields1771825341528"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "read_states" DROP COLUMN "public_ack"`); + await queryRunner.query(`ALTER TABLE "read_states" ADD "last_acked_id" character varying`); + await queryRunner.query(`ALTER TABLE "read_states" ADD "badge_count" integer NOT NULL DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE "read_states" ADD "read_state_type" integer NOT NULL DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE "read_states" ADD "flags" integer NOT NULL DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE "read_states" ALTER COLUMN "mention_count" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "read_states" ALTER COLUMN "mention_count" SET DEFAULT '0'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "read_states" ALTER COLUMN "mention_count" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "read_states" ALTER COLUMN "mention_count" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "read_states" DROP COLUMN "flags"`); + await queryRunner.query(`ALTER TABLE "read_states" DROP COLUMN "read_state_type"`); + await queryRunner.query(`ALTER TABLE "read_states" DROP COLUMN "badge_count"`); + await queryRunner.query(`ALTER TABLE "read_states" DROP COLUMN "last_acked_id"`); + await queryRunner.query(`ALTER TABLE "read_states" ADD "public_ack" character varying`); + } +}