Fixed notification filtering and a bunch of bugs
ci/woodpecker/push/ociImagePush Pipeline is running Details

This commit is contained in:
Natty 2024-02-29 00:26:09 +01:00
parent 7c2909bceb
commit a169ebb004
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
46 changed files with 495 additions and 279 deletions

View File

@ -153,35 +153,3 @@ pub enum UserProfileFfvisibilityEnum {
#[sea_orm(string_value = "public")] #[sea_orm(string_value = "public")]
Public, Public,
} }
#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Copy, Serialize, Deserialize)]
#[sea_orm(
rs_type = "String",
db_type = "Enum",
enum_name = "user_profile_mutingnotificationtypes_enum"
)]
pub enum UserProfileMutingnotificationtypesEnum {
#[sea_orm(string_value = "app")]
App,
#[sea_orm(string_value = "follow")]
Follow,
#[sea_orm(string_value = "followRequestAccepted")]
FollowRequestAccepted,
#[sea_orm(string_value = "groupInvited")]
GroupInvited,
#[sea_orm(string_value = "mention")]
Mention,
#[sea_orm(string_value = "pollEnded")]
PollEnded,
#[sea_orm(string_value = "pollVote")]
PollVote,
#[sea_orm(string_value = "quote")]
Quote,
#[sea_orm(string_value = "reaction")]
Reaction,
#[sea_orm(string_value = "receiveFollowRequest")]
ReceiveFollowRequest,
#[sea_orm(string_value = "renote")]
Renote,
#[sea_orm(string_value = "reply")]
Reply,
}

View File

@ -1,7 +1,7 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10 //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
use super::sea_orm_active_enums::NotificationTypeEnum;
use super::sea_orm_active_enums::UserProfileFfvisibilityEnum; use super::sea_orm_active_enums::UserProfileFfvisibilityEnum;
use super::sea_orm_active_enums::UserProfileMutingnotificationtypesEnum;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -51,7 +51,7 @@ pub struct Model {
#[sea_orm(column_name = "mutedWords", column_type = "JsonBinary")] #[sea_orm(column_name = "mutedWords", column_type = "JsonBinary")]
pub muted_words: Json, pub muted_words: Json,
#[sea_orm(column_name = "mutingNotificationTypes")] #[sea_orm(column_name = "mutingNotificationTypes")]
pub muting_notification_types: Vec<UserProfileMutingnotificationtypesEnum>, pub muting_notification_types: Vec<NotificationTypeEnum>,
#[sea_orm(column_name = "noCrawle")] #[sea_orm(column_name = "noCrawle")]
pub no_crawle: bool, pub no_crawle: bool,
#[sea_orm(column_name = "receiveAnnouncementEmail")] #[sea_orm(column_name = "receiveAnnouncementEmail")]

View File

@ -10,6 +10,7 @@ mod m20240107_224446_generated_is_renote;
mod m20240112_215106_remove_pages; mod m20240112_215106_remove_pages;
mod m20240112_234759_remove_gallery; mod m20240112_234759_remove_gallery;
mod m20240115_212109_remove_poll_vote_notification; mod m20240115_212109_remove_poll_vote_notification;
mod m20240228_155051_mag_notification_types_muting;
pub struct Migrator; pub struct Migrator;
@ -27,6 +28,7 @@ impl MigratorTrait for Migrator {
Box::new(m20240112_215106_remove_pages::Migration), Box::new(m20240112_215106_remove_pages::Migration),
Box::new(m20240112_234759_remove_gallery::Migration), Box::new(m20240112_234759_remove_gallery::Migration),
Box::new(m20240115_212109_remove_poll_vote_notification::Migration), Box::new(m20240115_212109_remove_poll_vote_notification::Migration),
Box::new(m20240228_155051_mag_notification_types_muting::Migration),
] ]
} }
} }

View File

@ -0,0 +1,43 @@
use sea_orm_migration::prelude::*;
use sea_orm_migration::sea_orm::TransactionTrait;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
db.execute_unprepared(
r#"
ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT;
ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "notification_type_enum"[] USING '{}'::notification_type_enum[];
ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'::notification_type_enum[];
DROP TYPE user_profile_mutingnotificationtypes_enum;
"#,
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
let txn = db.begin().await?;
db.execute_unprepared(
r#"
CREATE TYPE user_profile_mutingnotificationtypes_enum AS ENUM ('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app');
ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT;
ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "user_profile_mutingnotificationtypes_enum"[] USING '{}'::user_profile_mutingnotificationtypes_enum[];
ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'::user_profile_mutingnotificationtypes_enum[];
"#,
)
.await?;
txn.commit().await?;
Ok(())
}
}

View File

@ -62,7 +62,7 @@ impl FromQueryResult for NoteData {
let prefix = if prefix.is_empty() { let prefix = if prefix.is_empty() {
note::Entity.base_prefix() note::Entity.base_prefix()
} else { } else {
MagIden::alias(&prefix) MagIden::alias(prefix)
}; };
Ok(NoteData { Ok(NoteData {

View File

@ -95,7 +95,7 @@ impl NotificationResolver {
note_resolver: &NoteResolver, note_resolver: &NoteResolver,
user_resolver: &UserResolver, user_resolver: &UserResolver,
) { ) {
q.add_aliased_columns::<notification::Entity>(&notification_tbl); q.add_aliased_columns::<notification::Entity>(notification_tbl);
let notifier_tbl = notification_tbl.join_str(NOTIFIER); let notifier_tbl = notification_tbl.join_str(NOTIFIER);
q.add_aliased_columns::<user::Entity>(&notifier_tbl); q.add_aliased_columns::<user::Entity>(&notifier_tbl);
@ -143,7 +143,7 @@ impl NotificationResolver {
self.resolve( self.resolve(
&mut query, &mut query,
&notification_tbl, &notification_tbl,
&resolve_options, resolve_options,
&self.note_resolver, &self.note_resolver,
&self.user_resolver, &self.user_resolver,
); );
@ -183,7 +183,7 @@ impl NotificationResolver {
self.resolve( self.resolve(
&mut query, &mut query,
&notification_tbl, &notification_tbl,
&resolve_options, resolve_options,
&self.note_resolver, &self.note_resolver,
&self.user_resolver, &self.user_resolver,
); );

View File

@ -1,95 +1,96 @@
{ {
"name": "client", "name": "client",
"private": true, "private": true,
"scripts": { "scripts": {
"watch": "pnpm vite build --watch --mode development", "watch": "pnpm vite build --watch --mode development",
"watchRebuild": "pnpm vite build --watch", "watchRebuild": "pnpm vite build --watch",
"build": "pnpm vite build", "build": "pnpm vite build",
"format": "pnpm prettier --write '**/*.vue'" "format": "pnpm prettier --write '**/*.vue'"
}, },
"devDependencies": { "devDependencies": {
"@discordapp/twemoji": "14.1.2", "@discordapp/twemoji": "14.1.2",
"@phosphor-icons/web": "^2.0.3", "@phosphor-icons/web": "^2.0.3",
"@rollup/plugin-alias": "3.1.9", "@rollup/plugin-alias": "3.1.9",
"@rollup/plugin-json": "4.1.0", "@rollup/plugin-json": "4.1.0",
"@rollup/pluginutils": "^4.2.1", "@rollup/pluginutils": "^4.2.1",
"@types/escape-regexp": "0.0.1", "@types/escape-regexp": "0.0.1",
"@types/glob": "8.1.0", "@types/glob": "8.1.0",
"@types/gulp": "4.0.11", "@types/gulp": "4.0.11",
"@types/gulp-rename": "2.0.2", "@types/gulp-rename": "2.0.2",
"@types/katex": "0.16.0", "@types/katex": "0.16.0",
"@types/matter-js": "0.18.2", "@types/matter-js": "0.18.2",
"@types/punycode": "2.1.0", "@types/punycode": "2.1.0",
"@types/seedrandom": "3.0.5", "@types/seedrandom": "3.0.5",
"@types/throttle-debounce": "5.0.0", "@types/throttle-debounce": "5.0.0",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
"@types/uuid": "8.3.4", "@types/uuid": "8.3.4",
"@vitejs/plugin-vue": "4.2.3", "@vitejs/plugin-vue": "4.2.3",
"@vue/compiler-sfc": "3.3.4", "@vue/compiler-sfc": "3.3.4",
"@vue/runtime-core": "3.3.4", "@vue/runtime-core": "3.3.4",
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
"autosize": "5.0.2", "autosize": "5.0.2",
"blurhash": "1.1.5", "blurhash": "1.1.5",
"broadcast-channel": "4.19.1", "broadcast-channel": "4.19.1",
"browser-image-resizer": "github:misskey-dev/browser-image-resizer", "browser-image-resizer": "github:misskey-dev/browser-image-resizer",
"calckey-js": "workspace:*", "calckey-js": "workspace:*",
"chart.js": "4.3.0", "chart.js": "4.3.0",
"chartjs-adapter-date-fns": "3.0.0", "chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "^2.0.1", "chartjs-chart-matrix": "^2.0.1",
"chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.1", "chartjs-plugin-zoom": "2.0.1",
"city-timezones": "^1.2.1", "city-timezones": "^1.2.1",
"compare-versions": "5.0.3", "compare-versions": "5.0.3",
"cropperjs": "2.0.0-beta.2", "cropperjs": "2.0.0-beta.2",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "10.11.0", "cypress": "10.11.0",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"emojilib": "github:thatonecalculator/emojilib", "emojilib": "github:thatonecalculator/emojilib",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"eventemitter3": "4.0.7", "eventemitter3": "4.0.7",
"focus-trap": "^7.4.3", "focus-trap": "^7.4.3",
"focus-trap-vue": "^4.0.2", "focus-trap-vue": "^4.0.2",
"gsap": "^3.11.5", "gsap": "^3.11.5",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
"insert-text-at-cursor": "0.3.0", "insert-text-at-cursor": "0.3.0",
"json5": "2.2.3", "json5": "2.2.3",
"katex": "0.16.7", "katex": "0.16.7",
"magnetar-common": "workspace:*", "magnetar-common": "workspace:*",
"matter-js": "0.18.0", "matter-js": "0.18.0",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"photoswipe": "5.3.7", "photoswipe": "5.3.7",
"prettier": "2.8.8", "prettier": "2.8.8",
"prettier-plugin-vue": "1.1.6", "prettier-plugin-vue": "1.1.6",
"prismjs": "1.29.0", "prismjs": "1.29.0",
"punycode": "2.1.1", "punycode": "2.1.1",
"querystring": "0.2.1", "querystring": "0.2.1",
"rndstr": "1.0.0", "rndstr": "1.0.0",
"rollup": "3.23.1", "rollup": "3.23.1",
"s-age": "1.1.2", "s-age": "1.1.2",
"sass": "1.62.1", "sass": "1.62.1",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"start-server-and-test": "1.15.2", "start-server-and-test": "1.15.2",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"swiper": "9.3.2", "swiper": "9.3.2",
"syuilo-password-strength": "0.0.1", "syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.146.0", "three": "0.146.0",
"throttle-debounce": "5.0.0", "throttle-debounce": "5.0.0",
"tinycolor2": "1.5.2", "tinycolor2": "1.5.2",
"tsc-alias": "1.8.6", "tsc-alias": "1.8.6",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typescript": "5.1.3", "typescript": "5.1.3",
"unicode-emoji-json": "^0.4.0", "unicode-emoji-json": "^0.4.0",
"uuid": "9.0.0", "uuid": "9.0.0",
"vanilla-tilt": "1.8.0", "vanilla-tilt": "1.8.0",
"vite": "4.3.9", "vite": "4.3.9",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vue": "3.3.4", "vue": "3.3.4",
"vue-isyourpasswordsafe": "^2.0.0", "vue-component-type-helpers": "1.8.27",
"vue-plyr": "^7.0.0", "vue-isyourpasswordsafe": "^2.0.0",
"vue3-otp-input": "^0.4.1", "vue-plyr": "^7.0.0",
"vuedraggable": "4.1.0" "vue3-otp-input": "^0.4.1",
} "vuedraggable": "4.1.0"
}
} }

View File

@ -25,13 +25,13 @@
notification.type === 'Mention' || notification.type === 'Mention' ||
notification.type === 'Reply') notification.type === 'Reply')
" "
:key="notification.id" :key="'noteNotif' + notification.id"
:note="notification.note" :note="notification.note"
:collapsedReply="!!notification.note.parent_note" :collapsedReply="!!notification.note.parent_note"
/> />
<XNotification <XNotification
v-else v-else
:key="notification.id" :key="'basicNotif' + notification.id"
:notification="notification" :notification="notification"
:with-time="true" :with-time="true"
:full="true" :full="true"
@ -43,14 +43,15 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, ref } from "vue"; import { onMounted, onUnmounted, reactive, ref, watch } from "vue";
import type { ComponentExposed } from "vue-component-type-helpers";
import XNotification from "@/components/MagNotification.vue"; import XNotification from "@/components/MagNotification.vue";
import XList from "@/components/MkDateSeparatedList.vue"; import XList from "@/components/MkDateSeparatedList.vue";
import XNote from "@/components/MagNote.vue"; import XNote from "@/components/MagNote.vue";
import { magStream, stream } from "@/stream"; import { magStream, stream } from "@/stream";
import { $i } from "@/account"; import { $i } from "@/account";
import MagPagination, { Paging } from "@/components/MagPagination.vue"; import MagPagination, { Paging } from "@/components/MagPagination.vue";
import { endpoints, types } from "magnetar-common"; import { endpoints, packed, types } from "magnetar-common";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { ChannelEvent } from "magnetar-common/built/types"; import { ChannelEvent } from "magnetar-common/built/types";
@ -59,10 +60,16 @@ const props = defineProps<{
unreadOnly?: boolean; unreadOnly?: boolean;
}>(); }>();
const pagingComponent = ref<InstanceType<typeof MagPagination>>(); const pagingComponent =
ref<
ComponentExposed<
typeof MagPagination<typeof endpoints.GetNotifications>
>
>();
const pagination: Paging = { const pagination: Paging<typeof endpoints.GetNotifications> = reactive({
endpoint: endpoints.GetNotifications, endpoint: endpoints.GetNotifications,
pathParams: {},
params: { params: {
include_types: props.includeTypes ?? undefined, include_types: props.includeTypes ?? undefined,
exclude_types: props.includeTypes exclude_types: props.includeTypes
@ -70,9 +77,16 @@ const pagination: Paging = {
: $i?.mutingNotificationTypes, : $i?.mutingNotificationTypes,
unread_only: props.unreadOnly, unread_only: props.unreadOnly,
} as types.NotificationsReq, } as types.NotificationsReq,
}; });
const onNotification = (notification) => { watch(
() => props.includeTypes,
(types) => {
pagination.params!.include_types = types ?? undefined;
}
);
const onNotification = (notification: packed.PackNotification) => {
const isMuted = props.includeTypes const isMuted = props.includeTypes
? !props.includeTypes.includes(notification.type) ? !props.includeTypes.includes(notification.type)
: $i?.mutingNotificationTypes?.includes(notification.type); : $i?.mutingNotificationTypes?.includes(notification.type);
@ -83,7 +97,7 @@ const onNotification = (notification) => {
} }
if (!isMuted) { if (!isMuted) {
pagingComponent.value.prepend({ pagingComponent.value?.prepend({
...notification, ...notification,
isRead: document.visibilityState === "visible", isRead: document.visibilityState === "visible",
}); });

View File

@ -73,15 +73,7 @@
T['paginated'] & true T['paginated'] & true
>" >"
> >
import { import { computed, isRef, onActivated, onDeactivated, ref, watch } from "vue";
computed,
ComputedRef,
isRef,
onActivated,
onDeactivated,
ref,
watch,
} from "vue";
import * as os from "@/os"; import * as os from "@/os";
import { import {
getScrollContainer, getScrollContainer,
@ -99,16 +91,32 @@ import {
import { SpanFilter } from "magnetar-common/built/types/SpanFilter"; import { SpanFilter } from "magnetar-common/built/types/SpanFilter";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
type PathParams = { export type PathParams<
[K in keyof T["pathParams"] as T["pathParams"][K] & string]: P extends BackendApiEndpoint<
P["method"] & Method,
P["pathParams"] & string[],
P["request"],
P["response"],
P["paginated"] & true
>
> = {
[K in keyof P["pathParams"] as P["pathParams"][K] & string]:
| string | string
| number; | number;
}; };
export type Paging = { export type Paging<
endpoint: T; P extends BackendApiEndpoint<
pathParams: PathParams | ComputedRef<PathParams>; P["method"] & Method,
params?: T["request"] | ComputedRef<T["request"]>; P["pathParams"] & string[],
P["request"],
P["response"],
P["paginated"] & true
>
> = {
endpoint: P;
pathParams: PathParams<P>;
params?: P["request"];
limit?: number; limit?: number;
@ -117,7 +125,7 @@ export type Paging = {
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
pagination: Paging; pagination: Paging<T>;
disableAutoLoad?: boolean; disableAutoLoad?: boolean;
displayLimit?: number; displayLimit?: number;
}>(), }>(),
@ -146,12 +154,8 @@ const error = ref(false);
const fetch = async ( const fetch = async (
pagination: SpanFilter pagination: SpanFilter
): Promise<PaginatedResult<T["response"]> & { data: { id: string } }[]> => { ): Promise<PaginatedResult<T["response"]> & { data: { id: string } }[]> => {
const pathParams = isRef(props.pagination.pathParams) const pathParams = props.pagination.pathParams;
? props.pagination.pathParams.value const params = props.pagination.params;
: props.pagination.pathParams;
const params = isRef(props.pagination.params)
? props.pagination.params.value
: props.pagination.params;
return os return os
.magApi( .magApi(
@ -196,6 +200,8 @@ const reload = (): void => {
init(); init();
}; };
watch(props.pagination, reload);
const refresh = async (): Promise<void> => { const refresh = async (): Promise<void> => {
fetch({}).then( fetch({}).then(
(res) => { (res) => {

View File

@ -6,7 +6,7 @@
:with-ok-button="true" :with-ok-button="true"
:ok-button-disabled="false" :ok-button-disabled="false"
@ok="ok()" @ok="ok()"
@close="dialog.close()" @close="dialog?.close()"
@closed="emit('closed')" @closed="emit('closed')"
> >
<template #header>{{ i18n.ts.notificationSetting }}</template> <template #header>{{ i18n.ts.notificationSetting }}</template>
@ -31,7 +31,7 @@
v-for="ntype in notificationTypes" v-for="ntype in notificationTypes"
:key="ntype" :key="ntype"
v-model="typesMap[ntype]" v-model="typesMap[ntype]"
>{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch >{{ i18n.t(`_notification._magTypes.${ntype}`) }}</MkSwitch
> >
</div> </div>
</div> </div>
@ -40,25 +40,25 @@
<script lang="ts" setup> <script lang="ts" setup>
import {} from "vue"; import {} from "vue";
import { notificationTypes } from "calckey-js";
import MkSwitch from "./form/switch.vue"; import MkSwitch from "./form/switch.vue";
import MkInfo from "./MkInfo.vue"; import MkInfo from "./MkInfo.vue";
import MkButton from "./MkButton.vue"; import MkButton from "./MkButton.vue";
import XModalWindow from "@/components/MkModalWindow.vue"; import XModalWindow from "@/components/MkModalWindow.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { notificationTypes, types } from "magnetar-common";
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "done", v: { includingTypes: string[] | null }): void; done: [{ includingTypes: types.NotificationType[] | null }];
(ev: "closed"): void; closed: [];
}>(); }>();
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
includingTypes?: (typeof notificationTypes)[number][] | null; includingTypes?: types.NotificationType[] | null;
showGlobalToggle?: boolean; showGlobalToggle?: boolean;
}>(), }>(),
{ {
includingTypes: () => [], includingTypes: () => notificationTypes,
showGlobalToggle: true, showGlobalToggle: true,
} }
); );
@ -67,39 +67,35 @@ let includingTypes = $computed(() => props.includingTypes || []);
const dialog = $ref<InstanceType<typeof XModalWindow>>(); const dialog = $ref<InstanceType<typeof XModalWindow>>();
let typesMap = $ref<Record<(typeof notificationTypes)[number], boolean>>({}); let typesMap = $ref(
let useGlobalSetting = $ref( Object.fromEntries(
(includingTypes === null || includingTypes.length === 0) && notificationTypes.map((n) => [n, includingTypes.includes(n)])
props.showGlobalToggle ) as Record<types.NotificationType, boolean>
); );
for (const ntype of notificationTypes) { let useGlobalSetting = $ref(includingTypes === null || props.showGlobalToggle);
typesMap[ntype] = includingTypes.includes(ntype);
}
function ok() { function ok() {
if (useGlobalSetting) { if (useGlobalSetting) {
emit("done", { includingTypes: null }); emit("done", { includingTypes: null });
} else { } else {
emit("done", { emit("done", {
includingTypes: ( includingTypes: notificationTypes.filter((type) => typesMap[type]),
Object.keys(typesMap) as (typeof notificationTypes)[number][]
).filter((type) => typesMap[type]),
}); });
} }
dialog.close(); dialog?.close();
} }
function disableAll() { function disableAll() {
for (const type in typesMap) { for (const type in typesMap) {
typesMap[type as (typeof notificationTypes)[number]] = false; typesMap[type as types.NotificationType] = false;
} }
} }
function enableAll() { function enableAll() {
for (const type in typesMap) { for (const type in typesMap) {
typesMap[type as (typeof notificationTypes)[number]] = true; typesMap[type as types.NotificationType] = true;
} }
} }
</script> </script>

View File

@ -22,6 +22,7 @@ import {
types, types,
} from "magnetar-common"; } from "magnetar-common";
import { magReactionToLegacy } from "@/scripts-mag/mag-util"; import { magReactionToLegacy } from "@/scripts-mag/mag-util";
import { ComponentProps } from "vue-component-type-helpers";
export const pendingApiRequestsCount = ref(0); export const pendingApiRequestsCount = ref(0);
@ -307,11 +308,49 @@ export function getUniqueId(): string {
return uniqueId++ + ""; return uniqueId++ + "";
} }
export async function popup( // See https://github.com/misskey-dev/misskey/blob/5f43c2faa2fae3866a9921d81ab43c3b9e8bd222/packages/frontend/src/os.ts#L150
component: Component, // We cannot use "vue-component-type-helpers"'s ComponentEmit, because it returns a type intersection, making it useless
props: Record<string, any>, // for type checking of emit handlers
events = {}, //
disposeEvent?: string // We're not sure why the props were picked from T, because it didn't work, so we made it work on $props, which seems to work correctly
type ComponentEmit<T> = T extends new () => { $props: infer Props }
? [keyof Pick<Props, Extract<keyof Props, `on${string}`>>] extends [never]
? Record<string, unknown>
: EmitsExtractor<Props>
: T extends (...args: any) => any
? ReturnType<T> extends {
[x: string]: any;
__ctx?: { [x: string]: any; props: infer Props };
}
? [keyof Pick<Props, Extract<keyof Props, `on${string}`>>] extends [
never
]
? Record<string, unknown>
: EmitsExtractor<Props>
: never
: never;
type EmitsExtractor<T> = {
[K in keyof T as K extends `onVnode${string}`
? never
: K extends `on${infer E}`
? Uncapitalize<E>
: K extends string
? never
: K]: T[K];
};
type ComponentPropsRef<T extends Component> = {
[K in keyof ComponentProps<T>]:
| ComponentProps<T>[K]
| Ref<ComponentProps<T>[K]>;
};
export async function popup<C extends Component>(
component: C,
props: ComponentPropsRef<C>,
events: Partial<ComponentEmit<C>> = {} as ComponentEmit<C>,
disposeEvent?: keyof ComponentEmit<C>
) { ) {
markRaw(component); markRaw(component);
@ -1022,14 +1061,14 @@ export const deckGlobalEvents = new EventEmitter();
/* /*
export function checkExistence(fileData: ArrayBuffer): Promise<any> { export function checkExistence(fileData: ArrayBuffer): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const data = new FormData(); const data = new FormData();
data.append('md5', getMD5(fileData)); data.append('md5', getMD5(fileData));
os.api('drive/files/find-by-hash', { os.api('drive/files/find-by-hash', {
md5: getMD5(fileData) md5: getMD5(fileData)
}).then(resp => { }).then(resp => {
resolve(resp.length > 0 ? resp[0] : null); resolve(resp.length > 0 ? resp[0] : null);
}); });
}); });
}*/ }*/

View File

@ -61,7 +61,7 @@
import { computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import { Virtual } from "swiper"; import { Virtual } from "swiper";
import { Swiper, SwiperSlide } from "swiper/vue"; import { Swiper, SwiperSlide } from "swiper/vue";
import { notificationTypes } from "calckey-js"; import { notificationTypes, types } from "magnetar-common";
import XNotifications from "@/components/MagNotifications.vue"; import XNotifications from "@/components/MagNotifications.vue";
import XNotes from "@/components/MkNotes.vue"; import XNotes from "@/components/MkNotes.vue";
import * as os from "@/os"; import * as os from "@/os";
@ -76,7 +76,7 @@ const tabs = ["all", "unread", "mentions", "directNotes"];
let tab = $ref(tabs[0]); let tab = $ref(tabs[0]);
watch($$(tab), () => syncSlide(tabs.indexOf(tab))); watch($$(tab), () => syncSlide(tabs.indexOf(tab)));
let includeTypes = $ref<string[] | null>(null); let includeTypes = $ref<types.NotificationType[]>();
let unreadOnly = $computed(() => tab === "unread"); let unreadOnly = $computed(() => tab === "unread");
os.api("notifications/mark-all-as-read"); os.api("notifications/mark-all-as-read");
@ -104,7 +104,7 @@ const directNotesPagination = {
function setFilter(ev) { function setFilter(ev) {
const typeItems = notificationTypes.map((t) => ({ const typeItems = notificationTypes.map((t) => ({
text: i18n.t(`_notification._types.${t}`), text: i18n.t(`_notification._magTypes.${t}`),
active: includeTypes && includeTypes.includes(t), active: includeTypes && includeTypes.includes(t),
action: () => { action: () => {
includeTypes = [t]; includeTypes = [t];
@ -117,7 +117,7 @@ function setFilter(ev) {
icon: "ph-x ph-bold ph-lg", icon: "ph-x ph-bold ph-lg",
text: i18n.ts.clear, text: i18n.ts.clear,
action: () => { action: () => {
includeTypes = null; includeTypes = undefined;
}, },
}, },
null, null,
@ -191,6 +191,6 @@ function onSlideChange() {
} }
function syncSlide(index) { function syncSlide(index) {
swiperRef.slideTo(index); swiperRef?.slideTo(index);
} }
</script> </script>

View File

@ -47,7 +47,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent } from "vue"; import { defineAsyncComponent } from "vue";
import { notificationTypes } from "calckey-js";
import FormButton from "@/components/MkButton.vue"; import FormButton from "@/components/MkButton.vue";
import FormSection from "@/components/form/section.vue"; import FormSection from "@/components/form/section.vue";
import * as os from "@/os"; import * as os from "@/os";
@ -55,6 +54,12 @@ import { $i } from "@/account";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
import MkPushNotificationAllowButton from "@/components/MkPushNotificationAllowButton.vue"; import MkPushNotificationAllowButton from "@/components/MkPushNotificationAllowButton.vue";
import {
magLegacyNotificationType,
magNotificationType,
} from "@/scripts-mag/mag-util";
import { notificationTypes } from "magnetar-common";
import type * as Misskey from "calckey-js";
let allowButton = let allowButton =
$shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>(); $shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
@ -75,7 +80,14 @@ async function readAllNotifications() {
function configure() { function configure() {
const includingTypes = notificationTypes.filter( const includingTypes = notificationTypes.filter(
(x) => !$i!.mutingNotificationTypes.includes(x) (x) =>
!$i!.mutingNotificationTypes
.map((s) =>
magNotificationType(
s as Misskey.entities.Notification["type"]
)
)
.includes(x)
); );
os.popup( os.popup(
defineAsyncComponent( defineAsyncComponent(
@ -86,13 +98,14 @@ function configure() {
showGlobalToggle: false, showGlobalToggle: false,
}, },
{ {
done: async (res) => { done: async ({ includingTypes }) => {
const { includingTypes: value } = res;
await os await os
.apiWithDialog("i/update", { .apiWithDialog("i/update", {
mutingNotificationTypes: notificationTypes.filter( mutingNotificationTypes: notificationTypes
(x) => !value.includes(x) .filter((x) => !includingTypes!.includes(x))
), .map(magLegacyNotificationType)
.filter((s) => s)
.map((s) => s!),
}) })
.then((i) => { .then((i) => {
$i!.mutingNotificationTypes = i.mutingNotificationTypes; $i!.mutingNotificationTypes = i.mutingNotificationTypes;

View File

@ -149,6 +149,64 @@ export function magEffectiveNote(
return note.is_renote && note.renoted_note ? note.renoted_note : note; return note.is_renote && note.renoted_note ? note.renoted_note : note;
} }
export function magLegacyNotificationType(
nt: types.NotificationType | undefined
): Misskey.entities.Notification["type"] | undefined {
if (typeof nt === "undefined") return nt;
switch (nt) {
case "Reply":
return "reply";
case "Renote":
return "renote";
case "Reaction":
return "reaction";
case "Quote":
return "quote";
case "Mention":
return "mention";
case "Follow":
return "follow";
case "FollowRequestAccepted":
return "followRequestAccepted";
case "FollowRequestReceived":
return "receiveFollowRequest";
case "App":
return "app";
default:
return undefined;
}
}
export function magNotificationType(
nt: Misskey.entities.Notification["type"] | undefined
): types.NotificationType | undefined {
if (typeof nt === "undefined") return nt;
switch (nt) {
case "reply":
return "Reply";
case "renote":
return "Renote";
case "reaction":
return "Reaction";
case "quote":
return "Quote";
case "mention":
return "Mention";
case "follow":
return "Follow";
case "followRequestAccepted":
return "FollowRequestAccepted";
case "receiveFollowRequest":
return "FollowRequestReceived";
case "app":
return "App";
default:
return undefined;
}
}
export function magLegacyVisibility( export function magLegacyVisibility(
vis: types.NoteVisibility | Misskey.entities.Note["visibility"] vis: types.NoteVisibility | Misskey.entities.Note["visibility"]
): Misskey.entities.Note["visibility"]; ): Misskey.entities.Note["visibility"];

View File

@ -1,8 +1,8 @@
import { throttle } from "throttle-debounce"; import { throttle } from "throttle-debounce";
import { markRaw } from "vue"; import { markRaw } from "vue";
import { notificationTypes } from "calckey-js";
import { Storage } from "../../pizzax"; import { Storage } from "../../pizzax";
import { api } from "@/os"; import { api } from "@/os";
import { types } from "magnetar-common";
type ColumnWidget = { type ColumnWidget = {
name: string; name: string;
@ -13,15 +13,15 @@ type ColumnWidget = {
export type Column = { export type Column = {
id: string; id: string;
type: type:
| "main" | "main"
| "widgets" | "widgets"
| "notifications" | "notifications"
| "tl" | "tl"
| "antenna" | "antenna"
| "list" | "list"
| "mentions" | "mentions"
| "direct" | "direct"
| "bookmarks"; | "bookmarks";
name: string | null; name: string | null;
width: number; width: number;
widgets?: ColumnWidget[]; widgets?: ColumnWidget[];
@ -29,7 +29,7 @@ export type Column = {
flexible?: boolean; flexible?: boolean;
antennaId?: string; antennaId?: string;
listId?: string; listId?: string;
includingTypes?: (typeof notificationTypes)[number][]; includingTypes?: types.NotificationType[];
tl?: "home" | "local" | "social" | "global"; tl?: "home" | "local" | "social" | "global";
}; };
@ -316,9 +316,9 @@ export function updateColumnWidget(
column.widgets = column.widgets.map((w) => column.widgets = column.widgets.map((w) =>
w.id === widgetId w.id === widgetId
? { ? {
...w, ...w,
data: widgetData, data: widgetData,
} }
: w : w
); );
columns[columnIndex] = column; columns[columnIndex] = column;

View File

@ -10,18 +10,20 @@
>{{ column.name }}</template >{{ column.name }}</template
> >
<XNotifications :include-types="column.includingTypes" /> <XNotifications :include-types="includingTypes" />
</XColumn> </XColumn>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent } from "vue"; import { defineAsyncComponent, ref } from "vue";
import XColumn from "./column.vue"; import XColumn from "./column.vue";
import type { Column } from "./deck-store"; import type { Column } from "./deck-store";
import { updateColumn } from "./deck-store"; import { updateColumn } from "./deck-store";
import XNotifications from "@/components/MagNotifications.vue"; import XNotifications from "@/components/MagNotifications.vue";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import MkNotificationSettingWindow from "@/components/MkNotificationSettingWindow.vue";
import { types } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
column: Column; column: Column;
@ -32,8 +34,12 @@ const emit = defineEmits<{
(ev: "parent-focus", direction: "up" | "down" | "left" | "right"): void; (ev: "parent-focus", direction: "up" | "down" | "left" | "right"): void;
}>(); }>();
const includingTypes = ref<types.NotificationType[] | undefined>(
props.column.includingTypes
);
function func(): void { function func(): void {
os.popup( os.popup<typeof MkNotificationSettingWindow>(
defineAsyncComponent( defineAsyncComponent(
() => import("@/components/MkNotificationSettingWindow.vue") () => import("@/components/MkNotificationSettingWindow.vue")
), ),
@ -41,11 +47,11 @@ function func(): void {
includingTypes: props.column.includingTypes, includingTypes: props.column.includingTypes,
}, },
{ {
done: async (res) => { done: async ({ includingTypes: notifTypes }) => {
const { includingTypes } = res;
updateColumn(props.column.id, { updateColumn(props.column.id, {
includingTypes: includingTypes, includingTypes: notifTypes ?? undefined,
}); });
includingTypes.value = notifTypes ?? undefined;
}, },
}, },
"closed" "closed"

View File

@ -19,7 +19,9 @@
<i class="ph-gear-six ph-bold ph-lg"></i></button <i class="ph-gear-six ph-bold ph-lg"></i></button
></template> ></template>
<div> <div>
<XNotifications :include-types="widgetProps.includingTypes" /> <XNotifications
:include-types="widgetProps.includingTypes as types.NotificationType[]"
/>
</div> </div>
</MkContainer> </MkContainer>
</template> </template>
@ -30,8 +32,10 @@ import { useWidgetPropsManager, Widget, WidgetComponentExpose } from "./widget";
import { GetFormResultType } from "@/scripts/form"; import { GetFormResultType } from "@/scripts/form";
import MkContainer from "@/components/MkContainer.vue"; import MkContainer from "@/components/MkContainer.vue";
import XNotifications from "@/components/MagNotifications.vue"; import XNotifications from "@/components/MagNotifications.vue";
import MkNotificationSettingWindow from "@/components/MkNotificationSettingWindow.vue";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { types } from "magnetar-common";
const name = "notifications"; const name = "notifications";
@ -46,8 +50,8 @@ const widgetPropsDef = {
}, },
includingTypes: { includingTypes: {
type: "array" as const, type: "array" as const,
hidden: true,
default: null, default: null,
hidden: true as true,
}, },
}; };
@ -68,16 +72,16 @@ const { widgetProps, configure, save } = useWidgetPropsManager(
const configureNotification = () => { const configureNotification = () => {
os.popup( os.popup(
defineAsyncComponent( defineAsyncComponent<typeof MkNotificationSettingWindow>(
() => import("@/components/MkNotificationSettingWindow.vue") () => import("@/components/MkNotificationSettingWindow.vue")
), ),
{ {
includingTypes: widgetProps.includingTypes, includingTypes:
widgetProps.includingTypes as types.NotificationType[],
}, },
{ {
done: async (res) => { done: async ({ includingTypes }) => {
const { includingTypes } = res; widgetProps.includingTypes = includingTypes ?? undefined;
widgetProps.includingTypes = includingTypes;
save(); save();
}, },
}, },

View File

@ -2122,7 +2122,18 @@ _notification:
emptyPushNotificationMessage: "Push notifications have been updated" emptyPushNotificationMessage: "Push notifications have been updated"
reacted: "reacted to your post" reacted: "reacted to your post"
renoted: "boosted your post" renoted: "boosted your post"
voted: "voted on your poll" _magTypes:
All: "All"
Follow: "New followers"
Mention: "Mentions"
Reply: "Replies"
Renote: "Boosts"
Quote: "Quotes"
Reaction: "Reactions"
PollEnd: "Polls ending"
FollowRequestReceived: "Received follow requests"
FollowRequestAccepted: "Accepted follow requests"
App: "Notifications from linked apps"
_types: _types:
all: "All" all: "All"
follow: "New followers" follow: "New followers"

View File

@ -19,6 +19,19 @@ import * as types from "./types";
import * as packed from "./packed"; import * as packed from "./packed";
import * as endpoints from "./endpoints"; import * as endpoints from "./endpoints";
export const notificationTypes: types.NotificationType[] = [
"Follow",
"FollowRequestReceived",
"FollowRequestAccepted",
"Mention",
"Reply",
"Renote",
"Reaction",
"Quote",
"PollEnd",
"App",
];
export { export {
Method, Method,
BackendApiEndpoint, BackendApiEndpoint,

View File

@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { NotificationSettings } from "./NotificationSettings"; import type { NotificationSettings } from "./NotificationSettings";
export interface UserSelfExt { avatar_id: string | null, banner_id: string | null, email_announcements: boolean, always_mark_sensitive: boolean, reject_bot_follow_requests: boolean, reject_crawlers: boolean, reject_ai_training: boolean, has_unread_announcements: boolean, has_unread_antenna: boolean, has_unread_notifications: boolean, has_pending_follow_requests: boolean, word_mutes: Array<string>, instance_mutes: Array<string>, notification_settings: NotificationSettings, } export interface UserSelfExt { avatar_id: string | null, banner_id: string | null, email_announcements: boolean, always_mark_sensitive: boolean, reject_bot_follow_requests: boolean, reject_crawlers: boolean, reject_ai_training: boolean, word_mutes: Array<string>, instance_mutes: Array<string>, notification_settings: NotificationSettings, }

View File

@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface UserSelfReq { profile?: boolean, pins?: boolean, detail?: boolean, secrets?: boolean, } export interface UserSelfReq { profile?: boolean, pins?: boolean, detail?: boolean, secrets?: boolean, self_detail?: boolean, }

View File

@ -3,7 +3,7 @@ import type { Empty } from "../Empty";
import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll"; import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll";
export const GetFollowRequestsSelf = { export const GetFollowRequestsSelf = {
endpoint: "/users/@self/follow-requests", endpoint: "/users/@self/follow-requests" as "/users/@self/follow-requests",
pathParams: [] as [], pathParams: [] as [],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as Empty, request: undefined as unknown as Empty,

View File

@ -3,7 +3,7 @@ import type { Empty } from "../Empty";
import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll"; import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll";
export const GetFollowersById = { export const GetFollowersById = {
endpoint: "/users/:id/followers", endpoint: "/users/:id/followers" as "/users/:id/followers",
pathParams: ["id"] as ["id"], pathParams: ["id"] as ["id"],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as Empty, request: undefined as unknown as Empty,

View File

@ -3,7 +3,7 @@ import type { Empty } from "../Empty";
import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll"; import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll";
export const GetFollowersSelf = { export const GetFollowersSelf = {
endpoint: "/users/@self/followers", endpoint: "/users/@self/followers" as "/users/@self/followers",
pathParams: [] as [], pathParams: [] as [],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as Empty, request: undefined as unknown as Empty,

View File

@ -3,7 +3,7 @@ import type { Empty } from "../Empty";
import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll"; import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll";
export const GetFollowingById = { export const GetFollowingById = {
endpoint: "/users/:id/following", endpoint: "/users/:id/following" as "/users/:id/following",
pathParams: ["id"] as ["id"], pathParams: ["id"] as ["id"],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as Empty, request: undefined as unknown as Empty,

View File

@ -3,7 +3,7 @@ import type { Empty } from "../Empty";
import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll"; import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll";
export const GetFollowingSelf = { export const GetFollowingSelf = {
endpoint: "/users/@self/following", endpoint: "/users/@self/following" as "/users/@self/following",
pathParams: [] as [], pathParams: [] as [],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as Empty, request: undefined as unknown as Empty,

View File

@ -3,7 +3,7 @@ import type { ManyUsersByIdReq } from "../ManyUsersByIdReq";
import type { PackUserBase } from "../packed/PackUserBase"; import type { PackUserBase } from "../packed/PackUserBase";
export const GetManyUsersById = { export const GetManyUsersById = {
endpoint: "/users/lookup-many", endpoint: "/users/lookup-many" as "/users/lookup-many",
pathParams: [] as [], pathParams: [] as [],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as ManyUsersByIdReq, request: undefined as unknown as ManyUsersByIdReq,

View File

@ -3,7 +3,7 @@ import type { NoteByIdReq } from "../NoteByIdReq";
import type { PackNoteMaybeFull } from "../packed/PackNoteMaybeFull"; import type { PackNoteMaybeFull } from "../packed/PackNoteMaybeFull";
export const GetNoteById = { export const GetNoteById = {
endpoint: "/notes/:id", endpoint: "/notes/:id" as "/notes/:id",
pathParams: ["id"] as ["id"], pathParams: ["id"] as ["id"],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as NoteByIdReq, request: undefined as unknown as NoteByIdReq,

View File

@ -3,7 +3,7 @@ import type { NotificationsReq } from "../NotificationsReq";
import type { PackNotification } from "../PackNotification"; import type { PackNotification } from "../PackNotification";
export const GetNotifications = { export const GetNotifications = {
endpoint: "/users/@self/notifications", endpoint: "/users/@self/notifications" as "/users/@self/notifications",
pathParams: [] as [], pathParams: [] as [],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as NotificationsReq, request: undefined as unknown as NotificationsReq,

View File

@ -3,7 +3,7 @@ import type { GetTimelineReq } from "../GetTimelineReq";
import type { PackNoteMaybeFull } from "../packed/PackNoteMaybeFull"; import type { PackNoteMaybeFull } from "../packed/PackNoteMaybeFull";
export const GetTimeline = { export const GetTimeline = {
endpoint: "/timeline/:timeline_type", endpoint: "/timeline/:timeline_type" as "/timeline/:timeline_type",
pathParams: ["timeline_type"] as ["timeline_type"], pathParams: ["timeline_type"] as ["timeline_type"],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as GetTimelineReq, request: undefined as unknown as GetTimelineReq,

View File

@ -3,7 +3,7 @@ import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll";
import type { UserByIdReq } from "../UserByIdReq"; import type { UserByIdReq } from "../UserByIdReq";
export const GetUserByAcct = { export const GetUserByAcct = {
endpoint: "/users/by-acct/:user_acct", endpoint: "/users/by-acct/:user_acct" as "/users/by-acct/:user_acct",
pathParams: ["user_acct"] as ["user_acct"], pathParams: ["user_acct"] as ["user_acct"],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as UserByIdReq, request: undefined as unknown as UserByIdReq,

View File

@ -3,7 +3,7 @@ import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll";
import type { UserByIdReq } from "../UserByIdReq"; import type { UserByIdReq } from "../UserByIdReq";
export const GetUserById = { export const GetUserById = {
endpoint: "/users/:user_id", endpoint: "/users/:user_id" as "/users/:user_id",
pathParams: ["user_id"] as ["user_id"], pathParams: ["user_id"] as ["user_id"],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as UserByIdReq, request: undefined as unknown as UserByIdReq,

View File

@ -3,7 +3,7 @@ import type { PackUserSelfMaybeAll } from "../packed/PackUserSelfMaybeAll";
import type { UserSelfReq } from "../UserSelfReq"; import type { UserSelfReq } from "../UserSelfReq";
export const GetUserSelf = { export const GetUserSelf = {
endpoint: "/users/@self", endpoint: "/users/@self" as "/users/@self",
pathParams: [] as [], pathParams: [] as [],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH", method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as UserSelfReq, request: undefined as unknown as UserSelfReq,

View File

@ -5,5 +5,6 @@ import type { UserDetailExt } from "../UserDetailExt";
import type { UserProfileExt } from "../UserProfileExt"; import type { UserProfileExt } from "../UserProfileExt";
import type { UserProfilePinsEx } from "../UserProfilePinsEx"; import type { UserProfilePinsEx } from "../UserProfilePinsEx";
import type { UserSecretsExt } from "../UserSecretsExt"; import type { UserSecretsExt } from "../UserSecretsExt";
import type { UserSelfExt } from "../UserSelfExt";
export type PackUserSelfMaybeAll = Id & UserBase & Partial<UserProfileExt> & Partial<UserProfilePinsEx> & Partial<UserDetailExt> & Partial<UserSecretsExt>; export type PackUserSelfMaybeAll = Id & UserBase & Partial<UserProfileExt> & Partial<UserProfilePinsEx> & Partial<UserDetailExt> & Partial<UserSecretsExt> & Partial<UserSelfExt>;

View File

@ -344,6 +344,9 @@ importers:
vue: vue:
specifier: 3.3.4 specifier: 3.3.4
version: 3.3.4 version: 3.3.4
vue-component-type-helpers:
specifier: 1.8.27
version: 1.8.27
vue-isyourpasswordsafe: vue-isyourpasswordsafe:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0 version: 2.0.0
@ -1016,7 +1019,7 @@ packages:
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@swc/core': ^1.2.66 '@swc/core': ^1.2.66
chokidar: ^3.5.1 chokidar: ^3.3.1
peerDependenciesMeta: peerDependenciesMeta:
chokidar: chokidar:
optional: true optional: true
@ -7436,6 +7439,10 @@ packages:
fsevents: 2.3.3 fsevents: 2.3.3
dev: true dev: true
/vue-component-type-helpers@1.8.27:
resolution: {integrity: sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==}
dev: true
/vue-isyourpasswordsafe@2.0.0: /vue-isyourpasswordsafe@2.0.0:
resolution: {integrity: sha512-j3ORj18R9AgFiP2UOM35KuZbSeJAUiwCSyeRBFN3CGFYTJSKsxqU9qGqOHOz6OhLAYKMTin8JOmqugAbF9O+Bg==} resolution: {integrity: sha512-j3ORj18R9AgFiP2UOM35KuZbSeJAUiwCSyeRBFN3CGFYTJSKsxqU9qGqOHOz6OhLAYKMTin8JOmqugAbF9O+Bg==}
dependencies: dependencies:

View File

@ -59,7 +59,7 @@ fn split_tag_inner(tag: impl AsRef<str>) -> (String, Option<String>) {
let tag = tag.strip_prefix('@').unwrap_or(tag.as_ref()); let tag = tag.strip_prefix('@').unwrap_or(tag.as_ref());
match tag.split_once('@') { match tag.split_once('@') {
Some((name, host)) if name.is_empty() => (host.to_owned(), None), Some((name, "")) => (name.to_owned(), None),
Some((name, host)) => (name.to_owned(), Some(host.to_owned())), Some((name, host)) => (name.to_owned(), Some(host.to_owned())),
None => (tag.to_owned(), None), None => (tag.to_owned(), None),
} }

View File

@ -406,7 +406,7 @@ pub fn derive_endpoint(item: TokenStream) -> TokenStream {
fn decl() -> String { fn decl() -> String {
format!( format!(
"const {} = {{\n \ "const {} = {{\n \
endpoint: \"{}\",\n \ endpoint: \"{}\" as \"{}\",\n \
pathParams: {} as {},\n \ pathParams: {} as {},\n \
method: \"{}\" as \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\",\n \ method: \"{}\" as \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\",\n \
request: undefined as unknown as {},\n \ request: undefined as unknown as {},\n \
@ -416,6 +416,7 @@ pub fn derive_endpoint(item: TokenStream) -> TokenStream {
", ",
Self::name(), Self::name(),
Self::ENDPOINT, Self::ENDPOINT,
Self::ENDPOINT,
#path_params, #path_params,
#path_params, #path_params,
Self::METHOD, Self::METHOD,

View File

@ -20,6 +20,8 @@ pub struct UserSelfReq {
pub detail: Option<bool>, pub detail: Option<bool>,
#[ts(optional)] #[ts(optional)]
pub secrets: Option<bool>, pub secrets: Option<bool>,
#[ts(optional)]
pub self_detail: Option<bool>,
} }
#[derive(Endpoint)] #[derive(Endpoint)]

View File

@ -138,10 +138,6 @@ pub struct UserSelfExt {
pub reject_bot_follow_requests: bool, pub reject_bot_follow_requests: bool,
pub reject_crawlers: bool, pub reject_crawlers: bool,
pub reject_ai_training: bool, pub reject_ai_training: bool,
pub has_unread_announcements: bool,
pub has_unread_antenna: bool,
pub has_unread_notifications: bool,
pub has_pending_follow_requests: bool,
pub word_mutes: Vec<String>, pub word_mutes: Vec<String>,
pub instance_mutes: Vec<String>, pub instance_mutes: Vec<String>,
pub notification_settings: NotificationSettings, pub notification_settings: NotificationSettings,
@ -210,6 +206,7 @@ pack!(
& Optional<UserProfilePinsEx> as pins & Optional<UserProfilePinsEx> as pins
& Optional<UserDetailExt> as detail & Optional<UserDetailExt> as detail
& Optional<UserSecretsExt> as secrets & Optional<UserSecretsExt> as secrets
& Optional<UserSelfExt> as self_detail
); );
impl From<PackUserBase> for PackUserSelfMaybeAll { impl From<PackUserBase> for PackUserSelfMaybeAll {
@ -221,6 +218,7 @@ impl From<PackUserBase> for PackUserSelfMaybeAll {
Optional(None), Optional(None),
Optional(None), Optional(None),
Optional(None), Optional(None),
Optional(None),
)) ))
} }
} }

View File

@ -4,12 +4,14 @@ use magnetar_sdk::types::drive::PackDriveFileBase;
use magnetar_sdk::types::emoji::{EmojiContext, PackEmojiBase}; use magnetar_sdk::types::emoji::{EmojiContext, PackEmojiBase};
use magnetar_sdk::types::instance::InstanceTicker; use magnetar_sdk::types::instance::InstanceTicker;
use magnetar_sdk::types::note::PackNoteMaybeFull; use magnetar_sdk::types::note::PackNoteMaybeFull;
use magnetar_sdk::types::notification::NotificationType;
use magnetar_sdk::types::user::{ use magnetar_sdk::types::user::{
AvatarDecoration, MovedTo, PackSecurityKeyBase, ProfileField, SecurityKeyBase, SpeechTransform, AvatarDecoration, MovedTo, PackSecurityKeyBase, ProfileField, SecurityKeyBase, SpeechTransform,
UserAuthOverviewExt, UserBase, UserDetailExt, UserProfileExt, UserProfilePinsEx, UserAuthOverviewExt, UserBase, UserDetailExt, UserProfileExt, UserProfilePinsEx,
UserRelationExt, UserSecretsExt, UserRelationExt, UserSecretsExt, UserSelfExt,
}; };
use magnetar_sdk::types::MmXml; use magnetar_sdk::types::{MmXml, NotificationSettings};
use serde::Deserialize;
use url::Url; use url::Url;
use crate::model::{PackType, PackingContext}; use crate::model::{PackType, PackingContext};
@ -68,7 +70,7 @@ impl PackType<UserBaseSource<'_>> for UserBase {
is_moderator: user.is_moderator, is_moderator: user.is_moderator,
is_bot: user.is_bot, is_bot: user.is_bot,
emojis: emoji_context.clone(), emojis: emoji_context.clone(),
instance: instance.map(|i| i.clone()), instance: instance.cloned(),
} }
} }
} }
@ -111,10 +113,7 @@ impl PackType<UserProfileExtSource<'_>> for UserProfileExt {
description: profile.description.clone(), description: profile.description.clone(),
description_mm: description_mm.cloned(), description_mm: description_mm.cloned(),
location: profile.location.clone(), location: profile.location.clone(),
birthday: profile birthday: profile.birthday.clone().and_then(|b| b.parse().ok()),
.birthday
.clone()
.and_then(|b| b.parse().map_or_else(|_| None, Some)),
fields: profile_fields.clone(), fields: profile_fields.clone(),
follower_count: follow_visibility.then_some(user.followers_count as usize), follower_count: follow_visibility.then_some(user.followers_count as usize),
following_count: follow_visibility.then_some(user.following_count as usize), following_count: follow_visibility.then_some(user.following_count as usize),
@ -215,3 +214,29 @@ impl PackType<&ck::user_profile::Model> for UserAuthOverviewExt {
} }
} }
} }
impl PackType<(&ck::user::Model, &ck::user_profile::Model)> for UserSelfExt {
fn extract(
context: &PackingContext,
(user, profile): (&ck::user::Model, &ck::user_profile::Model),
) -> Self {
UserSelfExt {
avatar_id: user.avatar_id.clone(),
banner_id: user.banner_id.clone(),
email_announcements: profile.receive_announcement_email,
always_mark_sensitive: profile.always_mark_nsfw,
reject_bot_follow_requests: profile.careful_bot,
reject_crawlers: profile.no_crawle,
reject_ai_training: profile.prevent_ai_learning,
word_mutes: Vec::<String>::deserialize(&profile.muted_words).unwrap_or_else(|_| vec![]),
instance_mutes: vec![],
notification_settings: NotificationSettings {
enabled: profile
.muting_notification_types
.iter()
.map(|n| NotificationType::extract(context, n))
.collect::<Vec<_>>(),
},
}
}
}

View File

@ -137,6 +137,6 @@ impl PackingContext {
write.insert(link, relationship); write.insert(link, relationship);
drop(write); drop(write);
return Ok(relationship); Ok(relationship)
} }
} }

View File

@ -134,7 +134,7 @@ impl DriveModel {
Required(DriveFileBase::extract( Required(DriveFileBase::extract(
ctx, ctx,
PackFileBaseInput { PackFileBaseInput {
file: &file, file,
effective_url: url.as_ref(), effective_url: url.as_ref(),
effective_thumbnail_url: thumbnail_url.as_ref(), effective_thumbnail_url: thumbnail_url.as_ref(),
}, },

View File

@ -59,7 +59,7 @@ impl EmojiModel {
pub fn pack_existing(&self, ctx: &PackingContext, emoji: &ck::emoji::Model) -> PackEmojiBase { pub fn pack_existing(&self, ctx: &PackingContext, emoji: &ck::emoji::Model) -> PackEmojiBase {
PackEmojiBase::pack_from(( PackEmojiBase::pack_from((
Required(Id::from(&emoji.id)), Required(Id::from(&emoji.id)),
Required(EmojiBase::extract(ctx, &emoji)), Required(EmojiBase::extract(ctx, emoji)),
)) ))
} }
@ -70,7 +70,7 @@ impl EmojiModel {
host: Option<&str>, host: Option<&str>,
) -> PackResult<Vec<PackEmojiBase>> { ) -> PackResult<Vec<PackEmojiBase>> {
let emojis = ctx.service.emoji_cache.get_many(shortcodes, host).await?; let emojis = ctx.service.emoji_cache.get_many(shortcodes, host).await?;
let packed_emojis = emojis.iter().map(|e| self.pack_existing(ctx, &e)).collect(); let packed_emojis = emojis.iter().map(|e| self.pack_existing(ctx, e)).collect();
Ok(packed_emojis) Ok(packed_emojis)
} }
@ -81,7 +81,7 @@ impl EmojiModel {
tags: &[EmojiTag<'_>], tags: &[EmojiTag<'_>],
) -> PackResult<Vec<PackEmojiBase>> { ) -> PackResult<Vec<PackEmojiBase>> {
let emojis = ctx.service.emoji_cache.get_many_tagged(tags).await?; let emojis = ctx.service.emoji_cache.get_many_tagged(tags).await?;
let packed_emojis = emojis.iter().map(|e| self.pack_existing(ctx, &e)).collect(); let packed_emojis = emojis.iter().map(|e| self.pack_existing(ctx, e)).collect();
Ok(packed_emojis) Ok(packed_emojis)
} }

View File

@ -20,7 +20,7 @@ use magnetar_sdk::types::instance::InstanceTicker;
use magnetar_sdk::types::user::{ use magnetar_sdk::types::user::{
MovedTo, PackSecurityKeyBase, PackUserBase, PackUserMaybeAll, PackUserSelfMaybeAll, MovedTo, PackSecurityKeyBase, PackUserBase, PackUserMaybeAll, PackUserSelfMaybeAll,
ProfileField, SecurityKeyBase, UserAuthOverviewExt, UserBase, UserDetailExt, UserProfileExt, ProfileField, SecurityKeyBase, UserAuthOverviewExt, UserBase, UserDetailExt, UserProfileExt,
UserProfilePinsEx, UserRelationExt, UserRelationship, UserSecretsExt, UserProfilePinsEx, UserRelationExt, UserRelationship, UserSecretsExt, UserSelfExt,
}; };
use magnetar_sdk::types::{Id, MmXml}; use magnetar_sdk::types::{Id, MmXml};
use magnetar_sdk::{mmm, Optional, Packed, Required}; use magnetar_sdk::{mmm, Optional, Packed, Required};
@ -166,7 +166,7 @@ impl UserModel {
let base = UserBase::extract( let base = UserBase::extract(
ctx, ctx,
UserBaseSource { UserBaseSource {
user: &user, user,
username_mm: mmm::to_xml_string(&username_mm).map(MmXml).as_ref().ok(), username_mm: mmm::to_xml_string(&username_mm).map(MmXml).as_ref().ok(),
avatar_url, avatar_url,
avatar: avatar.as_ref(), avatar: avatar.as_ref(),
@ -243,14 +243,14 @@ impl UserModel {
None => None, None => None,
}; };
let description_mm = self.tokenize_description(&profile); let description_mm = self.tokenize_description(profile);
let fields = Vec::<ProfileFieldRaw>::deserialize(&profile.fields)?; let fields = Vec::<ProfileFieldRaw>::deserialize(&profile.fields)?;
let parser = mmm::Context::new(2); let parser = mmm::Context::new(2);
let profile_fields = fields let profile_fields = fields
.into_iter() .into_iter()
.map(|f| { .map(|f| {
let tok = parser.parse_profile_fields(&f.value); let tok = parser.parse_profile_fields(f.value);
ProfileField { ProfileField {
name: f.name.to_string(), name: f.name.to_string(),
@ -263,7 +263,7 @@ impl UserModel {
if let Some(desc_mm) = &description_mm { if let Some(desc_mm) = &description_mm {
let emoji_model = EmojiModel; let emoji_model = EmojiModel;
let shortcodes = emoji_model.deduplicate_emoji(ctx, get_mm_token_emoji(&desc_mm)); let shortcodes = emoji_model.deduplicate_emoji(ctx, get_mm_token_emoji(desc_mm));
let emojis = emoji_model let emojis = emoji_model
.fetch_many_emojis(ctx, &shortcodes, user.host.as_deref()) .fetch_many_emojis(ctx, &shortcodes, user.host.as_deref())
.await?; .await?;
@ -275,7 +275,7 @@ impl UserModel {
ctx, ctx,
UserProfileExtSource { UserProfileExtSource {
user, user,
profile: &profile, profile,
profile_fields: &profile_fields, profile_fields: &profile_fields,
banner: banner.as_ref(), banner: banner.as_ref(),
description_mm: description_mm description_mm: description_mm
@ -322,7 +322,9 @@ impl UserModel {
let user = user_data.user(); let user = user_data.user();
let should_fetch_profile = user_data.profile().is_none() let should_fetch_profile = user_data.profile().is_none()
&& (req.profile.unwrap_or_default() || req.secrets.unwrap_or_default()); && (req.profile.unwrap_or_default()
|| req.secrets.unwrap_or_default()
|| req.self_detail.unwrap_or_default());
let profile_raw_promise = let profile_raw_promise =
OptionFuture::from(should_fetch_profile.then(|| self.get_profile(ctx, user))); OptionFuture::from(should_fetch_profile.then(|| self.get_profile(ctx, user)));
let (base_res, profile_res) = let (base_res, profile_res) =
@ -370,6 +372,11 @@ impl UserModel {
.map(|notes| UserProfilePinsEx::extract(ctx, &notes)); .map(|notes| UserProfilePinsEx::extract(ctx, &notes));
let secrets_resolved = secrets_res.transpose()?; let secrets_resolved = secrets_res.transpose()?;
let self_info = req
.self_detail
.unwrap_or_default()
.then(|| UserSelfExt::extract(ctx, (&user, profile_ref.unwrap())));
Ok(PackUserSelfMaybeAll { Ok(PackUserSelfMaybeAll {
id: base.id, id: base.id,
user: base.user, user: base.user,
@ -377,6 +384,7 @@ impl UserModel {
pins: Optional(pins_resolved), pins: Optional(pins_resolved),
detail: Optional(detail), detail: Optional(detail),
secrets: Optional(secrets_resolved), secrets: Optional(secrets_resolved),
self_detail: Optional(self_info),
}) })
} }

View File

@ -31,7 +31,7 @@ pub fn serialize_as_urlenc(value: &Value) -> String {
.map(|(k, v)| { .map(|(k, v)| {
format!( format!(
"{}={}", "{}={}",
utf8_percent_encode(&k, NON_ALPHANUMERIC).to_string(), utf8_percent_encode(k, NON_ALPHANUMERIC),
if matches!(v, Value::Array(_) | Value::Object(_)) { if matches!(v, Value::Array(_) | Value::Object(_)) {
utf8_percent_encode(&serialize_as_urlenc(v), NON_ALPHANUMERIC) utf8_percent_encode(&serialize_as_urlenc(v), NON_ALPHANUMERIC)
.to_string() .to_string()

View File

@ -114,7 +114,7 @@ impl IntoResponseParts for Pagination {
let wrap = |uri: Uri, query: String, rel: Either<RelPrev, RelNext>| { let wrap = |uri: Uri, query: String, rel: Either<RelPrev, RelNext>| {
format!( format!(
"<{}?{}>; rel=\"{}\"", "<{}?{}>; rel=\"{}\"",
uri.to_string(), uri,
query, query,
rel.as_ref() rel.as_ref()
.map_either(RelPrev::as_ref, RelNext::as_ref) .map_either(RelPrev::as_ref, RelNext::as_ref)