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")]
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
use super::sea_orm_active_enums::NotificationTypeEnum;
use super::sea_orm_active_enums::UserProfileFfvisibilityEnum;
use super::sea_orm_active_enums::UserProfileMutingnotificationtypesEnum;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
@ -51,7 +51,7 @@ pub struct Model {
#[sea_orm(column_name = "mutedWords", column_type = "JsonBinary")]
pub muted_words: Json,
#[sea_orm(column_name = "mutingNotificationTypes")]
pub muting_notification_types: Vec<UserProfileMutingnotificationtypesEnum>,
pub muting_notification_types: Vec<NotificationTypeEnum>,
#[sea_orm(column_name = "noCrawle")]
pub no_crawle: bool,
#[sea_orm(column_name = "receiveAnnouncementEmail")]

View File

@ -10,6 +10,7 @@ mod m20240107_224446_generated_is_renote;
mod m20240112_215106_remove_pages;
mod m20240112_234759_remove_gallery;
mod m20240115_212109_remove_poll_vote_notification;
mod m20240228_155051_mag_notification_types_muting;
pub struct Migrator;
@ -27,6 +28,7 @@ impl MigratorTrait for Migrator {
Box::new(m20240112_215106_remove_pages::Migration),
Box::new(m20240112_234759_remove_gallery::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() {
note::Entity.base_prefix()
} else {
MagIden::alias(&prefix)
MagIden::alias(prefix)
};
Ok(NoteData {

View File

@ -95,7 +95,7 @@ impl NotificationResolver {
note_resolver: &NoteResolver,
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);
q.add_aliased_columns::<user::Entity>(&notifier_tbl);
@ -143,7 +143,7 @@ impl NotificationResolver {
self.resolve(
&mut query,
&notification_tbl,
&resolve_options,
resolve_options,
&self.note_resolver,
&self.user_resolver,
);
@ -183,7 +183,7 @@ impl NotificationResolver {
self.resolve(
&mut query,
&notification_tbl,
&resolve_options,
resolve_options,
&self.note_resolver,
&self.user_resolver,
);

View File

@ -87,6 +87,7 @@
"vite": "4.3.9",
"vite-plugin-compression": "^0.5.1",
"vue": "3.3.4",
"vue-component-type-helpers": "1.8.27",
"vue-isyourpasswordsafe": "^2.0.0",
"vue-plyr": "^7.0.0",
"vue3-otp-input": "^0.4.1",

View File

@ -25,13 +25,13 @@
notification.type === 'Mention' ||
notification.type === 'Reply')
"
:key="notification.id"
:key="'noteNotif' + notification.id"
:note="notification.note"
:collapsedReply="!!notification.note.parent_note"
/>
<XNotification
v-else
:key="notification.id"
:key="'basicNotif' + notification.id"
:notification="notification"
:with-time="true"
:full="true"
@ -43,14 +43,15 @@
</template>
<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 XList from "@/components/MkDateSeparatedList.vue";
import XNote from "@/components/MagNote.vue";
import { magStream, stream } from "@/stream";
import { $i } from "@/account";
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 { ChannelEvent } from "magnetar-common/built/types";
@ -59,10 +60,16 @@ const props = defineProps<{
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,
pathParams: {},
params: {
include_types: props.includeTypes ?? undefined,
exclude_types: props.includeTypes
@ -70,9 +77,16 @@ const pagination: Paging = {
: $i?.mutingNotificationTypes,
unread_only: props.unreadOnly,
} 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
? !props.includeTypes.includes(notification.type)
: $i?.mutingNotificationTypes?.includes(notification.type);
@ -83,7 +97,7 @@ const onNotification = (notification) => {
}
if (!isMuted) {
pagingComponent.value.prepend({
pagingComponent.value?.prepend({
...notification,
isRead: document.visibilityState === "visible",
});

View File

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

View File

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

View File

@ -22,6 +22,7 @@ import {
types,
} from "magnetar-common";
import { magReactionToLegacy } from "@/scripts-mag/mag-util";
import { ComponentProps } from "vue-component-type-helpers";
export const pendingApiRequestsCount = ref(0);
@ -307,11 +308,49 @@ export function getUniqueId(): string {
return uniqueId++ + "";
}
export async function popup(
component: Component,
props: Record<string, any>,
events = {},
disposeEvent?: string
// See https://github.com/misskey-dev/misskey/blob/5f43c2faa2fae3866a9921d81ab43c3b9e8bd222/packages/frontend/src/os.ts#L150
// We cannot use "vue-component-type-helpers"'s ComponentEmit, because it returns a type intersection, making it useless
// for type checking of emit handlers
//
// 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);

View File

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

View File

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

View File

@ -149,6 +149,64 @@ export function magEffectiveNote(
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(
vis: types.NoteVisibility | Misskey.entities.Note["visibility"]
): Misskey.entities.Note["visibility"];

View File

@ -1,8 +1,8 @@
import { throttle } from "throttle-debounce";
import { markRaw } from "vue";
import { notificationTypes } from "calckey-js";
import { Storage } from "../../pizzax";
import { api } from "@/os";
import { types } from "magnetar-common";
type ColumnWidget = {
name: string;
@ -29,7 +29,7 @@ export type Column = {
flexible?: boolean;
antennaId?: string;
listId?: string;
includingTypes?: (typeof notificationTypes)[number][];
includingTypes?: types.NotificationType[];
tl?: "home" | "local" | "social" | "global";
};

View File

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

View File

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

View File

@ -2122,7 +2122,18 @@ _notification:
emptyPushNotificationMessage: "Push notifications have been updated"
reacted: "reacted to 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:
all: "All"
follow: "New followers"

View File

@ -19,6 +19,19 @@ import * as types from "./types";
import * as packed from "./packed";
import * as endpoints from "./endpoints";
export const notificationTypes: types.NotificationType[] = [
"Follow",
"FollowRequestReceived",
"FollowRequestAccepted",
"Mention",
"Reply",
"Renote",
"Reaction",
"Quote",
"PollEnd",
"App",
];
export {
Method,
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.
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.
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";
export const GetFollowRequestsSelf = {
endpoint: "/users/@self/follow-requests",
endpoint: "/users/@self/follow-requests" as "/users/@self/follow-requests",
pathParams: [] as [],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as Empty,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll";
import type { UserByIdReq } from "../UserByIdReq";
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"],
method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: undefined as unknown as UserByIdReq,

View File

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

View File

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

View File

@ -5,5 +5,6 @@ import type { UserDetailExt } from "../UserDetailExt";
import type { UserProfileExt } from "../UserProfileExt";
import type { UserProfilePinsEx } from "../UserProfilePinsEx";
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:
specifier: 3.3.4
version: 3.3.4
vue-component-type-helpers:
specifier: 1.8.27
version: 1.8.27
vue-isyourpasswordsafe:
specifier: ^2.0.0
version: 2.0.0
@ -1016,7 +1019,7 @@ packages:
hasBin: true
peerDependencies:
'@swc/core': ^1.2.66
chokidar: ^3.5.1
chokidar: ^3.3.1
peerDependenciesMeta:
chokidar:
optional: true
@ -7436,6 +7439,10 @@ packages:
fsevents: 2.3.3
dev: true
/vue-component-type-helpers@1.8.27:
resolution: {integrity: sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==}
dev: true
/vue-isyourpasswordsafe@2.0.0:
resolution: {integrity: sha512-j3ORj18R9AgFiP2UOM35KuZbSeJAUiwCSyeRBFN3CGFYTJSKsxqU9qGqOHOz6OhLAYKMTin8JOmqugAbF9O+Bg==}
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());
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())),
None => (tag.to_owned(), None),
}

View File

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

View File

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

View File

@ -138,10 +138,6 @@ pub struct UserSelfExt {
pub reject_bot_follow_requests: bool,
pub reject_crawlers: 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 instance_mutes: Vec<String>,
pub notification_settings: NotificationSettings,
@ -210,6 +206,7 @@ pack!(
& Optional<UserProfilePinsEx> as pins
& Optional<UserDetailExt> as detail
& Optional<UserSecretsExt> as secrets
& Optional<UserSelfExt> as self_detail
);
impl From<PackUserBase> for PackUserSelfMaybeAll {
@ -221,6 +218,7 @@ impl From<PackUserBase> for PackUserSelfMaybeAll {
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::instance::InstanceTicker;
use magnetar_sdk::types::note::PackNoteMaybeFull;
use magnetar_sdk::types::notification::NotificationType;
use magnetar_sdk::types::user::{
AvatarDecoration, MovedTo, PackSecurityKeyBase, ProfileField, SecurityKeyBase, SpeechTransform,
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 crate::model::{PackType, PackingContext};
@ -68,7 +70,7 @@ impl PackType<UserBaseSource<'_>> for UserBase {
is_moderator: user.is_moderator,
is_bot: user.is_bot,
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_mm: description_mm.cloned(),
location: profile.location.clone(),
birthday: profile
.birthday
.clone()
.and_then(|b| b.parse().map_or_else(|_| None, Some)),
birthday: profile.birthday.clone().and_then(|b| b.parse().ok()),
fields: profile_fields.clone(),
follower_count: follow_visibility.then_some(user.followers_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);
drop(write);
return Ok(relationship);
Ok(relationship)
}
}

View File

@ -134,7 +134,7 @@ impl DriveModel {
Required(DriveFileBase::extract(
ctx,
PackFileBaseInput {
file: &file,
file,
effective_url: 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 {
PackEmojiBase::pack_from((
Required(Id::from(&emoji.id)),
Required(EmojiBase::extract(ctx, &emoji)),
Required(EmojiBase::extract(ctx, emoji)),
))
}
@ -70,7 +70,7 @@ impl EmojiModel {
host: Option<&str>,
) -> PackResult<Vec<PackEmojiBase>> {
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)
}
@ -81,7 +81,7 @@ impl EmojiModel {
tags: &[EmojiTag<'_>],
) -> PackResult<Vec<PackEmojiBase>> {
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)
}

View File

@ -20,7 +20,7 @@ use magnetar_sdk::types::instance::InstanceTicker;
use magnetar_sdk::types::user::{
MovedTo, PackSecurityKeyBase, PackUserBase, PackUserMaybeAll, PackUserSelfMaybeAll,
ProfileField, SecurityKeyBase, UserAuthOverviewExt, UserBase, UserDetailExt, UserProfileExt,
UserProfilePinsEx, UserRelationExt, UserRelationship, UserSecretsExt,
UserProfilePinsEx, UserRelationExt, UserRelationship, UserSecretsExt, UserSelfExt,
};
use magnetar_sdk::types::{Id, MmXml};
use magnetar_sdk::{mmm, Optional, Packed, Required};
@ -166,7 +166,7 @@ impl UserModel {
let base = UserBase::extract(
ctx,
UserBaseSource {
user: &user,
user,
username_mm: mmm::to_xml_string(&username_mm).map(MmXml).as_ref().ok(),
avatar_url,
avatar: avatar.as_ref(),
@ -243,14 +243,14 @@ impl UserModel {
None => None,
};
let description_mm = self.tokenize_description(&profile);
let description_mm = self.tokenize_description(profile);
let fields = Vec::<ProfileFieldRaw>::deserialize(&profile.fields)?;
let parser = mmm::Context::new(2);
let profile_fields = fields
.into_iter()
.map(|f| {
let tok = parser.parse_profile_fields(&f.value);
let tok = parser.parse_profile_fields(f.value);
ProfileField {
name: f.name.to_string(),
@ -263,7 +263,7 @@ impl UserModel {
if let Some(desc_mm) = &description_mm {
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
.fetch_many_emojis(ctx, &shortcodes, user.host.as_deref())
.await?;
@ -275,7 +275,7 @@ impl UserModel {
ctx,
UserProfileExtSource {
user,
profile: &profile,
profile,
profile_fields: &profile_fields,
banner: banner.as_ref(),
description_mm: description_mm
@ -322,7 +322,9 @@ impl UserModel {
let user = user_data.user();
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 =
OptionFuture::from(should_fetch_profile.then(|| self.get_profile(ctx, user)));
let (base_res, profile_res) =
@ -370,6 +372,11 @@ impl UserModel {
.map(|notes| UserProfilePinsEx::extract(ctx, &notes));
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 {
id: base.id,
user: base.user,
@ -377,6 +384,7 @@ impl UserModel {
pins: Optional(pins_resolved),
detail: Optional(detail),
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)| {
format!(
"{}={}",
utf8_percent_encode(&k, NON_ALPHANUMERIC).to_string(),
utf8_percent_encode(k, NON_ALPHANUMERIC),
if matches!(v, Value::Array(_) | Value::Object(_)) {
utf8_percent_encode(&serialize_as_urlenc(v), NON_ALPHANUMERIC)
.to_string()

View File

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