From 1691daa0008ed28b213aa15b397fec973210e4e7 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Tue, 18 Jun 2024 20:16:28 -1000 Subject: [PATCH] feat: :sparkles: Add settings page to configure account and preferences --- README.md | 4 +- components/buttons/base.vue | 4 +- components/composer/composer.vue | 62 +-------- components/headers/greeting.vue | 13 +- components/inputs/rich-textbox.vue | 85 +++++++++++++ components/settings/oidc.vue | 88 +++++++++++++ components/settings/profile-editor.vue | 120 ++++++++++++++++++ components/settings/renderer.vue | 47 +++++++ components/sidebars/account-picker.vue | 22 +++- components/sidebars/navigation.vue | 14 ++ components/sidebars/settings.vue | 33 +++++ components/social-elements/notes/note.vue | 3 +- .../social-elements/notifications/notif.vue | 10 +- components/social-elements/users/Account.vue | 41 ++---- composables/LinkedSSO.ts | 25 ++++ composables/NoteData.ts | 13 +- composables/ParsedContent.ts | 84 +++++++++--- composables/Settings.ts | 56 ++++++++ layouts/app.vue | 40 ++---- pages/settings/index.vue | 35 +++++ settings.ts | 71 ++++++++--- 21 files changed, 687 insertions(+), 183 deletions(-) create mode 100644 components/inputs/rich-textbox.vue create mode 100644 components/settings/oidc.vue create mode 100644 components/settings/profile-editor.vue create mode 100644 components/settings/renderer.vue create mode 100644 components/sidebars/settings.vue create mode 100644 composables/LinkedSSO.ts create mode 100644 composables/Settings.ts create mode 100644 pages/settings/index.vue diff --git a/README.md b/README.md index 28fbc22..03a01bd 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ - [x] Media uploads - [x] WCAG 2.2 AAA compliance - Testing is automated and may not catch all issues, please report any accessibility issues you find. -- [ ] Settings -- [ ] Profile editing +- [x] Settings +- [x] Profile editing ### Browser Support diff --git a/components/buttons/base.vue b/components/buttons/base.vue index 578b1c1..8df0d3c 100644 --- a/components/buttons/base.vue +++ b/components/buttons/base.vue @@ -1,8 +1,8 @@ \ No newline at end of file diff --git a/components/headers/greeting.vue b/components/headers/greeting.vue index 61431b2..0774c12 100644 --- a/components/headers/greeting.vue +++ b/components/headers/greeting.vue @@ -8,19 +8,18 @@

Welcome back,

-

+

- \ No newline at end of file diff --git a/components/inputs/rich-textbox.vue b/components/inputs/rich-textbox.vue new file mode 100644 index 0000000..bc1c778 --- /dev/null +++ b/components/inputs/rich-textbox.vue @@ -0,0 +1,85 @@ + + + \ No newline at end of file diff --git a/components/settings/oidc.vue b/components/settings/oidc.vue new file mode 100644 index 0000000..67a8f4f --- /dev/null +++ b/components/settings/oidc.vue @@ -0,0 +1,88 @@ + + + diff --git a/components/settings/profile-editor.vue b/components/settings/profile-editor.vue new file mode 100644 index 0000000..a69de8d --- /dev/null +++ b/components/settings/profile-editor.vue @@ -0,0 +1,120 @@ + + + diff --git a/components/settings/renderer.vue b/components/settings/renderer.vue new file mode 100644 index 0000000..d101f94 --- /dev/null +++ b/components/settings/renderer.vue @@ -0,0 +1,47 @@ + + + \ No newline at end of file diff --git a/components/sidebars/account-picker.vue b/components/sidebars/account-picker.vue index 7c45b25..b479669 100644 --- a/components/sidebars/account-picker.vue +++ b/components/sidebars/account-picker.vue @@ -12,7 +12,7 @@
{{ - currentIdentity.account.display_name }} + currentIdentity.account.display_name }}
@@ -44,13 +44,13 @@
{{ - identity.account.display_name }} + identity.account.display_name }}
@{{ - identity.account.acct - }} + identity.account.acct + }} +

Posts

diff --git a/components/sidebars/settings.vue b/components/sidebars/settings.vue new file mode 100644 index 0000000..f30e4bf --- /dev/null +++ b/components/sidebars/settings.vue @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/components/social-elements/notes/note.vue b/components/social-elements/notes/note.vue index 641dd32..f97653c 100644 --- a/components/social-elements/notes/note.vue +++ b/components/social-elements/notes/note.vue @@ -174,6 +174,7 @@ useListen("composer:send-edit", (note) => { const client = useClient(); const identity = useCurrentIdentity(); +const settings = useSettings(); const { loaded, note: outputtedNote, @@ -185,7 +186,7 @@ const { reblog, isReply, reblogDisplayName, -} = useNoteData(noteRef, client); +} = useNoteData(noteRef, client, settings); const openBlank = (url: string) => window.open(url, "_blank"); diff --git a/components/social-elements/notifications/notif.vue b/components/social-elements/notifications/notif.vue index 32c3b75..1d9c781 100644 --- a/components/social-elements/notifications/notif.vue +++ b/components/social-elements/notifications/notif.vue @@ -7,7 +7,7 @@ - {{ text + {{ text }} @@ -63,10 +63,12 @@ const rejectFollowRequest = async () => { isWorkingOnFollowRequest.value = false; }; -const accountName = useParsedContent( - props.notification?.account?.display_name ?? "", - props.notification?.account?.emojis ?? [], +const settings = useSettings(); +const { display_name } = useParsedAccount( + props.notification?.account, + settings, ); + const text = computed(() => { if (!props.notification) return ""; diff --git a/components/social-elements/users/Account.vue b/components/social-elements/users/Account.vue index 1a35391..c206309 100644 --- a/components/social-elements/users/Account.vue +++ b/components/social-elements/users/Account.vue @@ -27,7 +27,7 @@

- {{ account?.display_name }} + @@ -48,7 +48,7 @@ -
+
@@ -79,8 +79,8 @@
-
-
+
+
@@ -134,13 +134,6 @@ const formattedJoin = computed(() => }).format(new Date(props.account?.created_at ?? 0)), ); -const parsedNote = ref(""); -const parsedFields: Ref< - { - name: string; - value: string; - }[] -> = ref([]); const handle = computed(() => { if (!props.account?.acct.includes("@")) { return `${props.account?.acct}@${new URL(useBaseUrl().value).host}`; @@ -154,27 +147,9 @@ const visibleRoles = computed( () => props.account?.roles.filter((r) => r.visible) ?? [], ); -watch( - skeleton, - async () => { - if (skeleton.value) return; - parsedNote.value = - useParsedContent( - props.account?.note ?? "", - props.account?.emojis ?? [], - ).value ?? ""; - parsedFields.value = - props.account?.fields.map((field) => ({ - name: - useParsedContent(field.name, props.account?.emojis ?? []) - .value ?? "", - value: - useParsedContent(field.value, props.account?.emojis ?? []) - .value ?? "", - })) ?? []; - }, - { - immediate: true, - }, +const settings = useSettings(); +const { display_name, fields, note } = useParsedAccount( + computed(() => props.account), + settings, ); \ No newline at end of file diff --git a/composables/LinkedSSO.ts b/composables/LinkedSSO.ts new file mode 100644 index 0000000..0964546 --- /dev/null +++ b/composables/LinkedSSO.ts @@ -0,0 +1,25 @@ +import type { LysandClient } from "@lysand-org/client"; + +type SSOProvider = { + id: string; + name: string; + icon: string; +}; + +export const useLinkedSSO = (client: MaybeRef) => { + if (!client) { + return ref([] as SSOProvider[]); + } + + const output = ref([] as SSOProvider[]); + + watchEffect(() => { + toValue(client) + ?.get("/api/v1/sso") + .then((res) => { + output.value = res.data; + }); + }); + + return output; +}; diff --git a/composables/NoteData.ts b/composables/NoteData.ts index 9c5500e..e2feb2f 100644 --- a/composables/NoteData.ts +++ b/composables/NoteData.ts @@ -1,9 +1,11 @@ import type { LysandClient } from "@lysand-org/client"; +import { SettingIds, type Settings } from "~/settings"; import type { Status } from "~/types/mastodon/status"; export const useNoteData = ( noteProp: MaybeRef, client: Ref, + settings: MaybeRef, ) => { const isReply = computed(() => !!toValue(noteProp)?.in_reply_to_id); const isQuote = computed(() => !!toValue(noteProp)?.quote); @@ -15,11 +17,13 @@ export const useNoteData = ( ? toValue(noteProp)?.reblog ?? toValue(noteProp) : toValue(noteProp), ); + const showContentWarning = useSetting(SettingIds.ShowContentWarning); const shouldHide = computed( () => - renderedNote.value?.sensitive || - !!renderedNote.value?.spoiler_text || - false, + (renderedNote.value?.sensitive || + !!renderedNote.value?.spoiler_text || + false) && + (showContentWarning.value.value as boolean), ); const mentions = useResolveMentions( computed(() => renderedNote.value?.mentions ?? []), @@ -29,12 +33,15 @@ export const useNoteData = ( computed(() => renderedNote.value?.content ?? ""), computed(() => renderedNote.value?.emojis ?? []), mentions, + settings, ); const loaded = computed(() => content.value !== null); const reblogDisplayName = useParsedContent( toValue(noteProp)?.account.display_name ?? "", toValue(noteProp)?.account.emojis ?? [], + undefined, + settings, ); const reblog = computed(() => isReblog.value && toValue(noteProp) && !isQuote.value diff --git a/composables/ParsedContent.ts b/composables/ParsedContent.ts index 1d40223..4b7558f 100644 --- a/composables/ParsedContent.ts +++ b/composables/ParsedContent.ts @@ -1,4 +1,5 @@ import { renderToString } from "vue/server-renderer"; +import { SettingIds, type Settings, getSettingById } from "~/settings"; import type { Account } from "~/types/mastodon/account"; import type { Emoji } from "~/types/mastodon/emoji"; import MentionComponent from "../components/social-elements/notes/mention.vue"; @@ -13,6 +14,7 @@ export const useParsedContent = ( content: MaybeRef, emojis: MaybeRef, mentions: MaybeRef = ref([]), + settings: MaybeRef = ref([]), ): Ref => { const result = ref(null as string | null); @@ -26,28 +28,35 @@ export const useParsedContent = ( const contentHtml = document.createElement("div"); contentHtml.innerHTML = toValue(content); - // Replace emoji shortcodes with images - const paragraphs = contentHtml.querySelectorAll("p"); + const shouldRenderEmoji = getSettingById( + toValue(settings), + SettingIds.CustomEmojis, + )?.value; - for (const paragraph of paragraphs) { - paragraph.innerHTML = paragraph.innerHTML.replace( - /:([a-z0-9_-]+):/g, - (match, emoji) => { - const emojiData = toValue(emojis).find( - (e) => e.shortcode === emoji, - ); - if (!emojiData) { - return match; - } - const image = document.createElement("img"); - image.src = emojiData.url; - image.alt = `:${emoji}:`; - image.title = emojiData.shortcode; - image.className = - "h-6 align-text-bottom inline not-prose hover:scale-110 transition-transform duration-75 ease-in-out"; - return image.outerHTML; - }, - ); + // Replace emoji shortcodes with images + if (shouldRenderEmoji) { + const paragraphs = contentHtml.querySelectorAll("p"); + + for (const paragraph of paragraphs) { + paragraph.innerHTML = paragraph.innerHTML.replace( + /:([a-z0-9_-]+):/g, + (match, emoji) => { + const emojiData = toValue(emojis).find( + (e) => e.shortcode === emoji, + ); + if (!emojiData) { + return match; + } + const image = document.createElement("img"); + image.src = emojiData.url; + image.alt = `:${emoji}:`; + image.title = emojiData.shortcode; + image.className = + "h-6 align-text-bottom inline not-prose hover:scale-110 transition-transform duration-75 ease-in-out"; + return image.outerHTML; + }, + ); + } } // Replace links containing mentions with interactive mentions @@ -76,3 +85,36 @@ export const useParsedContent = ( return result; }; + +export const useParsedAccount = ( + account: MaybeRef, + settings: MaybeRef, +) => { + const display_name = computed(() => toValue(account)?.display_name ?? ""); + const note = computed(() => toValue(account)?.note ?? ""); + const fields = computed(() => toValue(account)?.fields ?? []); + const emojis = computed(() => toValue(account)?.emojis ?? []); + + const parsedDisplayName = useParsedContent( + display_name, + emojis, + undefined, + settings, + ); + + const parsedNote = useParsedContent(note, emojis, undefined, settings); + + const parsedFields = computed(() => + fields.value.map((field) => ({ + ...field, + value: useParsedContent(field.value, emojis, undefined, settings) + .value, + })), + ); + + return { + display_name: parsedDisplayName, + note: parsedNote, + fields: parsedFields, + }; +}; diff --git a/composables/Settings.ts b/composables/Settings.ts new file mode 100644 index 0000000..171db1c --- /dev/null +++ b/composables/Settings.ts @@ -0,0 +1,56 @@ +import { StorageSerializers } from "@vueuse/core"; +import { + type Setting, + type SettingIds, + type Settings, + parseFromJson, + settings, +} from "~/settings"; + +export const useSettings = () => { + return useLocalStorage("lysand:settings", settings, { + serializer: { + read(raw) { + const json = StorageSerializers.object.read(raw); + + return parseFromJson(json); + }, + write(value) { + // key/value, with key being id and value being the value + const json = value.reduce( + (acc, setting) => { + acc[setting.id] = setting.value; + return acc; + }, + {} as Record, + ); + + return StorageSerializers.object.write(json); + }, + }, + }); +}; + +export const useSetting = (id: SettingIds) => { + const settings = useSettings(); + + const setting: Ref = ref( + settings.value.find((s) => s.id === id) as T, + ) as unknown as Ref; + + watch(settings, (newSettings) => { + setting.value = newSettings.find((s) => s.id === id) as T; + }); + + watch(setting, (newSetting) => { + settings.value = settings.value.map((s) => + s.id === id ? newSetting : s, + ) as Settings; + }); + + return setting; +}; + +export const getSetting = (id: SettingIds) => { + return settings.find((s) => s.id === id) as T; +}; diff --git a/layouts/app.vue b/layouts/app.vue index 1d34626..572cd45 100644 --- a/layouts/app.vue +++ b/layouts/app.vue @@ -45,44 +45,20 @@ import { OverlayScrollbarsComponent } from "#imports"; const { width } = useWindowSize(); -const { n, o_i_d_c } = useMagicKeys(); +const { n } = useMagicKeys(); +const activeElement = useActiveElement(); +const notUsingInput = computed( + () => + activeElement.value?.tagName !== "INPUT" && + activeElement.value?.tagName !== "TEXTAREA", +); const identity = useCurrentIdentity(); -const client = useClient(); -const providers = useSSOConfig(); watchEffect(async () => { - if (n?.value) { + if (n?.value && notUsingInput.value) { // Wait 50ms await new Promise((resolve) => setTimeout(resolve, 50)); useEvent("composer:open"); } - if (o_i_d_c?.value) { - useEvent("notification:new", { - type: "progress", - title: "Linking your account", - persistent: true, - }); - - const issuer = providers.value?.providers[0]; - - if (!issuer) { - console.error("No SSO provider found"); - return; - } - - const response = await fetch(new URL("/api/v1/sso", client.value.url), { - method: "POST", - headers: { - Authorization: `Bearer ${identity.value?.tokens.access_token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - issuer: issuer.id, - }), - }); - - const json = await response.json(); - window.location.href = json.link; - } }); \ No newline at end of file diff --git a/pages/settings/index.vue b/pages/settings/index.vue new file mode 100644 index 0000000..76c338a --- /dev/null +++ b/pages/settings/index.vue @@ -0,0 +1,35 @@ + + + \ No newline at end of file diff --git a/settings.ts b/settings.ts index 11ad320..118df87 100644 --- a/settings.ts +++ b/settings.ts @@ -11,6 +11,7 @@ export type Setting = { id: string; title: string; description: string; + notImplemented?: boolean; type: T; value: T extends SettingType.String | SettingType.Code ? string @@ -29,30 +30,59 @@ export type Setting = { path: SettingPages; }; -export type Settings = Setting[]; export enum SettingPages { + Account = "account", Behaviour = "behaviour", - Appearance = "appearance", Advanced = "advanced", + Appearance = "appearance", } -export const getSettingsForPath = (settings: Settings, path: string) => - settings.filter((setting) => setting.path === path); +export const getSettingsForPath = ( + settingsToFilterIn: Settings, + path: SettingPages, +) => settingsToFilterIn.filter((setting) => setting.path === path); -export const getSettingById = (settings: Settings, id: string) => - settings.find((setting) => setting.id === id); +export const getSettingById = (settingsToFilterIn: Settings, id: SettingIds) => + settingsToFilterIn.find((setting) => setting.id === id); -export const settings: Settings = [ +export const parseFromJson = (json: Record) => { + const finalSettings = structuredClone(settings); + + // Override the default values with the values from the JSON except for the user value + for (const setting of finalSettings) { + if (setting.id in json) { + setting.value = json[setting.id] as (typeof setting)["value"]; + } + } + + return finalSettings; +}; + +export enum SettingIds { + MFM = "mfm", + CustomCSS = "custom-css", + Theme = "theme", + CustomEmojis = "custom-emojis", + ShowContentWarning = "show-content-warning", + PopupAvatarHover = "popup-avatar-hover", + ConfirmDelete = "confirm-delete", + ConfirmFollow = "confirm-follow", + ConfirmReblog = "confirm-reblog", + ConfirmFavourite = "confirm-favourite", +} + +export const settings = [ { - id: "mfm", + id: SettingIds.MFM, title: "Render MFM", description: "Render Misskey-Flavoured Markdown", type: SettingType.Boolean, value: false, path: SettingPages.Behaviour, + notImplemented: true, } as Setting, { - id: "custom-css", + id: SettingIds.CustomCSS, title: "Custom CSS", description: "Custom CSS for the UI", type: SettingType.Code, @@ -61,7 +91,7 @@ export const settings: Settings = [ path: SettingPages.Appearance, } as Setting, { - id: "theme", + id: SettingIds.Theme, title: "Theme", description: "UI theme", type: SettingType.Enum, @@ -70,7 +100,7 @@ export const settings: Settings = [ path: SettingPages.Appearance, } as Setting, { - id: "custom-emojis", + id: SettingIds.CustomEmojis, title: "Render Custom Emojis", description: "Render custom emojis", type: SettingType.Boolean, @@ -78,7 +108,7 @@ export const settings: Settings = [ path: SettingPages.Behaviour, } as Setting, { - id: "show-content-warning", + id: SettingIds.ShowContentWarning, title: "Show Content Warning", description: "Show content warnings on notes marked sensitive/spoiler", type: SettingType.Boolean, @@ -86,43 +116,50 @@ export const settings: Settings = [ path: SettingPages.Behaviour, } as Setting, { - id: "popup-avatar-hover", + id: SettingIds.PopupAvatarHover, title: "Popup Profile Hover", description: "Show profile popup when hovering over a user's avatar", type: SettingType.Boolean, value: true, path: SettingPages.Behaviour, + notImplemented: true, } as Setting, { - id: "confirm-delete", + id: SettingIds.ConfirmDelete, title: "Confirm Delete", description: "Confirm before deleting a note", type: SettingType.Boolean, value: false, path: SettingPages.Behaviour, + notImplemented: true, } as Setting, { - id: "confirm-follow", + id: SettingIds.ConfirmFollow, title: "Confirm Follow", description: "Confirm before following/unfollowing a user", type: SettingType.Boolean, value: false, path: SettingPages.Behaviour, + notImplemented: true, } as Setting, { - id: "confirm-reblog", + id: SettingIds.ConfirmReblog, title: "Confirm Reblog", description: "Confirm before reblogging a note", type: SettingType.Boolean, value: false, path: SettingPages.Behaviour, + notImplemented: true, } as Setting, { - id: "confirm-favourite", + id: SettingIds.ConfirmFavourite, title: "Confirm Favourite", description: "Confirm before favouriting a note", type: SettingType.Boolean, value: false, path: SettingPages.Behaviour, + notImplemented: true, } as Setting, ]; + +export type Settings = typeof settings;