Skip to content

Commit

Permalink
feat: add admin panel and links settings
Browse files Browse the repository at this point in the history
  • Loading branch information
Cl0v1s committed Feb 8, 2025
1 parent 2d0310a commit d5ec3f0
Show file tree
Hide file tree
Showing 15 changed files with 203 additions and 15 deletions.
15 changes: 15 additions & 0 deletions akko/src/akkoma/entities/v1/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ export interface AccountField {
verifiedAt?: string | null;
}

/**
* Represents akkoma/pleroma related meta-data about the user
*/
export interface AccountPleroma {
/**
* user's instance favicon
*/
favicon: string;
/**
* Is user admin of its instance
*/
isAdmin: boolean;
}

/**
* Represents a user of Mastodon and their associated profile.
* @see https://docs.joinmastodon.org/entities/account/
Expand Down Expand Up @@ -95,6 +109,7 @@ export interface Account {
roles: Pick<Role, "id" | "name" | "color">[]; // TODO: Create an entity when documentation is updated
/** https://github.com/mastodon/mastodon/pull/23591 */
memorial?: boolean | null;
pleroma: AccountPleroma;
}

/**
Expand Down
7 changes: 5 additions & 2 deletions akko/src/akkoma/entities/v1/pleroma-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export type FrontendConfiguration = { [x: string]: { [x: string]: unknown } };
export type FrontendConfiguration =
| { [x: string]: unknown }
| unknown[]
| { tuple: unknown[] }
| string;

export interface PleromaConfig {
/**
Expand All @@ -11,7 +15,6 @@ export interface PleromaConfig {
configs: {
group: string;
key: string;
db: string[];
value: FrontendConfiguration[];
}[];
}
12 changes: 11 additions & 1 deletion akko/src/akkoma/rest/v1/pleroma-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@ import {
type PleromaConfig,
} from "../../entities/v1/pleroma-config";

export interface CreateAdminConfigParams {
configs: PleromaConfig["configs"];
}

export interface PleromaRepository {
admin: {
config: {
/** Retrieves current pleroma configuration */
fetch(): Promise<PleromaConfig>;
/**
* Update current pleroma configuration
* (yeah the name is create but that's how things works what do you want)
*/
create(params: CreateAdminConfigParams): Promise<PleromaConfig>;
};
};

/** Fetch frontend configuration */
frontendConfigurations: {
fetch(): Promise<FrontendConfiguration>;
fetch(): Promise<{ [x: string]: FrontendConfiguration }>;
};
}
96 changes: 96 additions & 0 deletions elk/components/administration/AdministrationLinks.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<script lang="ts" setup>
const { config, update } = useFrontendConfig()
const links = ref(config.value?.links || [])
const isLoading = ref(false)
watch(config, () => {
if (config.value)
links.value = config.value?.links
})
function add() {
links.value = [...links.value, { icon: '🔗', text: '', url: '' }]
}
function save() {
const ls = links.value.filter(l => l.url && l.text)
update({ links: ls }, isLoading)
links.value = ls
}
function insertEmoji(url: string, emoji: string) {
const ls = [...links.value]
const index = ls.findIndex(l => l.url === url)
ls.splice(index, 1, { ...ls[index], icon: emoji })
links.value = ls
}
function onChange(e: Event, link: FrontendConfiguration['links'][number]) {
const ls = [...links.value]
const index = ls.findIndex(l => l.url === link.url)
const data = new FormData((e.currentTarget as HTMLFieldSetElement).form!)
ls.splice(index, 1, { ...ls[index], text: data.get(`${link.url}-text`) as string, url: data.get(`${link.url}-url`) as string })
links.value = ls
}
</script>

<template>
<form @submit.prevent="save">
<h2 font-medium>
{{ $t('admin.links.title') }}
</h2>
<p my-2>
{{ $t('admin.links.description') }}
</p>
<div flex flex-col gap-3>
<template v-for="link in links" :key="link.url">
<div flex items-center gap-3>
<EmojiPicker :hide-custom-emojis="true" @select="(emoji) => insertEmoji(link.url, emoji)">
<button grayscale input-base w-auto h-full type="button">
{{ link.icon }}
</button>
</EmojiPicker>
<fieldset grow flex items-center gap-3 @change="(e) => onChange(e, link)">
<input
:value="link.text"
:name="`${link.url}-text`"
shrink-1
type="text" placeholder-text-secondary
:placeholder="$t('admin.links.label')"
input-base
w-auto
>
<input
:value="link.url"
:name="`${link.url}-url`"
grow
type="text" placeholder-text-secondary
:placeholder="$t('admin.links.url')"
w-auto
input-base
pattern="https?://.*"
>
</fieldset>
</div>
</template>
<button
btn-outline font-bold py2 full-w sm-wa flex="~ gap2 center"
type="button"
@click="add"
>
<span aria-hidden="true" class="block i-ri:add-line" />
{{ $t('admin.links.add') }}
</button>
</div>
<button
btn-outline font-bold py2 full-w sm-wa flex="~ gap2 center" ml-auto mt-3
type="submit"
:disabled="isLoading"
:class="isLoading ? 'border-none' : undefined"
>
<span v-if="!isLoading" aria-hidden="true" class="block i-ri:save-2-line" />
<span v-else i-ri:loader-4-line animate-spin />
{{ $t('action.save') }}
</button>
</form>
</template>
6 changes: 4 additions & 2 deletions elk/components/emoji/EmojiPicker.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import type { Picker } from 'emoji-mart'
import importEmojiLang from 'virtual:emoji-mart-lang-importer'
const { hideCustomEmojis } = defineProps<{ hideCustomEmojis?: boolean }>()
const emit = defineEmits<{
(e: 'select', code: string): void
(e: 'selectCustom', image: any): void
Expand All @@ -19,7 +21,7 @@ async function openEmojiPicker() {
if (picker.value) {
picker.value.update({
theme: colorMode,
custom: customEmojisData.value,
custom: !hideCustomEmojis ? customEmojisData.value : null,
})
}
else {
Expand All @@ -36,7 +38,7 @@ async function openEmojiPicker() {
emit('selectCustom', { src, alt, 'data-emoji-id': name })
},
theme: colorMode,
custom: customEmojisData.value,
custom: !hideCustomEmojis ? customEmojisData.value : null,
i18n,
})
}
Expand Down
2 changes: 2 additions & 0 deletions elk/components/nav/NavSide.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { STORAGE_KEY_LAST_ACCESSED_NOTIFICATION_ROUTE } from '~/constants'
import NavSideItem from './NavSideItem.vue'
const { command } = defineProps<{
command?: boolean
Expand Down Expand Up @@ -49,6 +50,7 @@ function composeNavigate() {
<NavSideItem :text="$t('nav.hashtags')" to="/hashtags" icon="i-ri:hashtag" user-only :command="command" />
<hr border-gray-700 my-4>
<NavSideItem :text="$t('nav.settings')" to="/settings" icon="i-ri:settings-3-line" :command="command" />
<NavSideItem v-if="currentUser?.account.pleroma.isAdmin" :text="$t('nav.admin')" to="/administration" icon="i-ri:admin-line" :command="command" />
<div flex-auto />
<button
v-if="noUserVisual"
Expand Down
20 changes: 20 additions & 0 deletions elk/composables/akko/frontend.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { akkoma } from '@bdxtown/akko'
import { name } from './../../package.json'

export interface FrontendConfiguration {
links: {
Expand All @@ -15,7 +16,26 @@ export function useFrontendConfig() {
{ watch: [isHydrated], immediate: isHydrated.value, default: () => shallowRef(undefined) },
)

const client = useAkkoClient()

async function update(configuration: FrontendConfiguration, isLoading: Ref<boolean> = ref(false)) {
isLoading.value = true
try {
const response = await client.v1.pleroma.admin.config.create({
configs: [{ group: ':pleroma', key: ':frontend_configurations', value: [{ tuple: [`:${name.replace(/[-@/]/g, '')}_fe`, { ...configuration }] }] }],
})
isLoading.value = false
return response
}
catch (e) {
console.error(e)
// TODO: proper error handling
}
isLoading.value = false
}

return {
config,
update,
}
}
7 changes: 1 addition & 6 deletions elk/composables/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,7 @@ export function fetchFrontendConfiguration(): Promise<FrontendConfiguration | un
return Promise.resolve(cache.get(key))

const promise = useAkkoClient().pleroma.frontendConfigurations.fetch().then((config) => {
let frontendConfiguration = config[`${name}Fe`] || config[`${'soapbox'}Fe`] // TODO: remove when settings will be ok
// TODO: remove when settings will be ok
frontendConfiguration = {
...frontendConfiguration,
links: (frontendConfiguration.promoPanel as { items: FrontendConfiguration['links'] }).items,
}
const frontendConfiguration = config[`${name.replace(/[-@/]/g, '')}Fe`]
cacheFrontendConfiguration(frontendConfiguration)
return frontendConfiguration as unknown as FrontendConfiguration | undefined
})
Expand Down
10 changes: 10 additions & 0 deletions elk/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@
"switch_account": "Switch account",
"vote": "Vote"
},
"admin": {
"links": {
"add": "Add link",
"description": "You can enter links to display to the users of this interface.",
"label": "Label",
"title": "Links",
"url": "URL"
}
},
"app_desc_short": "A nimble Akkoma web client",
"app_logo": "Elk Logo",
"app_name": "Elk",
Expand Down Expand Up @@ -304,6 +313,7 @@
"aria_label_close": "Close"
},
"nav": {
"admin": "Administration",
"back": "Go back",
"blocked_domains": "Blocked domains",
"blocked_users": "Blocked users",
Expand Down
10 changes: 10 additions & 0 deletions elk/locales/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@
"switch_account": "Changer de compte",
"vote": "Voter"
},
"admin": {
"links": {
"add": "Ajouter",
"description": "Vous pouvez ajouter des liens à afficher aux utilisateurs de cette interface",
"label": "Label",
"title": "Raccourcis",
"url": "URL"
}
},
"app_desc_short": "Un client Mastodon fait avec 🧡",
"app_logo": "Logo Elk",
"app_name": "Elk",
Expand Down Expand Up @@ -273,6 +282,7 @@
"aria_label_close": "Fermer"
},
"nav": {
"admin": "Administration",
"back": "Retourner à la page précédente",
"blocked_domains": "Domaines bloqués",
"blocked_users": "Comptes bloqués",
Expand Down
2 changes: 1 addition & 1 deletion elk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"release": "stale-dep && bumpp && tsx scripts/release.ts"
},
"dependencies": {
"@bdxtown/akko": "^6.15.2",
"@bdxtown/akko": "^6.15.3",
"@emoji-mart/data": "^1.1.2",
"@fnando/sparkline": "^0.3.10",
"@iconify-emoji/twemoji": "^1.0.2",
Expand Down
25 changes: 25 additions & 0 deletions elk/pages/administration.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script lang="ts" setup>
definePageMeta({
wideLayout: true,
})
const { t } = useI18n()
useHydratedHead({
title: () => t('nav.admin'),
})
</script>

<template>
<div>
<div min-h-screen flex flex-col gap-3 p-5>
<div timeline-title-style flex items-center gap-2 @click="$scrollToTop">
<div i-ri:admin-line />
<span>{{ $t('nav.admin') }}</span>
</div>
<div>
<AdministrationLinks />
</div>
</div>
</div>
</template>
2 changes: 1 addition & 1 deletion elk/server/api/[server]/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default defineEventHandler(async (event) => {
const query = stringifyQuery({
client_id: app.client_id,
force_login: force_login === true ? 'true' : 'false',
scope: 'read write follow push',
scope: 'read write follow push admin',
response_type: 'code',
lang,
redirect_uri: getRedirectURI(origin, server),
Expand Down
2 changes: 1 addition & 1 deletion elk/server/api/[server]/oauth/[origin].ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default defineEventHandler(async (event) => {
redirect_uri: getRedirectURI(origin, server),
grant_type: 'authorization_code',
code,
scope: 'read write follow push',
scope: 'read write follow push admin',
},
retry: 3,
})
Expand Down
2 changes: 1 addition & 1 deletion elk/server/utils/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ async function fetchAppInfo(origin: string, server: string) {
client_name: APP_NAME + (env !== 'release' ? ` (${env})` : ''),
website: 'https://elk.zone',
redirect_uris: getRedirectURI(origin, server),
scopes: 'read write follow push',
scopes: 'read write follow push admin',
},
})
return app
Expand Down

0 comments on commit d5ec3f0

Please sign in to comment.