Skip to content
4 changes: 2 additions & 2 deletions packages/frontend-main/src/components/tma/TMAInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ onMounted(async () => {

const tmaInfo = computed(() => {
const params = tmaStore.launchParams;
const initData = tmaStore.initData;
const user = initData?.user();
const initDataValue = tmaStore.initData;
const user = initDataValue?.user;

if (!user) {
return {
Expand Down
57 changes: 57 additions & 0 deletions packages/frontend-main/src/composables/useSEO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// TODO: Consider migrating to @unhead/vue for reactivity-first SEO management
// See: https://unhead.unjs.io/
// This would provide better SSR support and reactive composables that could be extended to other pages.

interface SeoOptions {
title?: string;
description?: string;
ogTitle?: string;
ogDescription?: string;
ogUrl?: string;
}

function upsertMeta(
attributes: Record<string, string>,
) {
if (typeof document === 'undefined') return;

const selectorParts = Object.entries(attributes)
.filter(([key]) => key === 'name' || key === 'property')
.map(([key, value]) => `[${key}="${value}"]`);

const selector = selectorParts.join('');
let element = selector ? document.head.querySelector<HTMLMetaElement>(`meta${selector}`) : null;

if (!element) {
element = document.createElement('meta');
document.head.appendChild(element);
}

Object.entries(attributes).forEach(([key, value]) => {
element?.setAttribute(key, value);
});
}

export function useSEO(options: SeoOptions) {
if (typeof document === 'undefined') return;

if (options.title) {
document.title = options.title;
upsertMeta({ property: 'og:title', content: options.ogTitle ?? options.title });
}

if (options.description) {
upsertMeta({ name: 'description', content: options.description });
}

if (options.ogDescription || options.description) {
upsertMeta({
property: 'og:description',
content: options.ogDescription ?? options.description ?? '',
});
}

if (options.ogUrl) {
upsertMeta({ property: 'og:url', content: options.ogUrl });
}
}
10 changes: 10 additions & 0 deletions packages/frontend-main/src/config/externalLinks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const externalLinks = {
siteUrl: 'https://dither.chat',
protocolDocs: 'https://docs.dither.chat',
docsHome: 'https://docs.dither.chat',
privacyPolicy: 'https://docs.dither.chat/policies/privacy-policy',
termsOfService: 'https://docs.dither.chat/policies/terms-of-service',
telegramBot: 'https://t.me/dither_chat_bot',
githubRepo: 'https://github.com/allinbits/dither.chat',
atomoneWebsite: 'https://atom.one/',
};
11 changes: 9 additions & 2 deletions packages/frontend-main/src/layouts/panels/RightPanel.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';

import FilterPhoton from '@/components/ui/filter/FilterPhoton.vue';
import { SearchInput } from '@/components/ui/search';
import ColorModeSwitch from '@/components/ui/switch/ColorModeSwitch.vue';
import { routesNames } from '@/router';

const router = useRouter();
</script>

<template>
<aside class="flex flex-col h-full w-full max-w-[358px] gap-8 p-6">
<ColorModeSwitch />
<FilterPhoton />
<SearchInput />

<FilterPhoton v-if="router.currentRoute.value.name !== routesNames.about" />

<SearchInput v-if="router.currentRoute.value.name !== routesNames.about" />
</aside>
</template>
10 changes: 9 additions & 1 deletion packages/frontend-main/src/localization/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,15 @@ export const messages = {
About: {
privacyPolicy: 'Privacy Policy',
termsAndConditions: 'Terms & Conditions',
docs: 'Documentation',
heroTagline: 'Social protocol for decentralized networking',
atomoneLink: 'AtomOne',
coreDesc: 'Anyone can run the service or build their own client.',
ctaDocs: 'Protocol Docs',
ctaPrivacy: 'Privacy Policy',
ctaTerms: 'Terms & Conditions',
ctaGitHub: 'Source Code',
ctaTelegram: 'Telegram Bot',
copyright: 'All in Bits',
},
Authz: {
title: 'Authz',
Expand Down
12 changes: 3 additions & 9 deletions packages/frontend-main/src/stores/useTMAStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { InitData, RetrieveLaunchParamsError, RetrieveLaunchParamsResult } from '@tma.js/sdk-vue';
import type { RetrieveLaunchParamsResult } from '@tma.js/sdk-vue';

import {
initData,
Expand All @@ -22,7 +22,7 @@ declare global {
export const useTMAStore = defineStore('tmaStore', () => {
const isInitialized = ref(false);
const launchParams = ref<RetrieveLaunchParamsResult | null>(null);
const initDataRef = ref<InitData<RetrieveLaunchParamsError>>();
const initDataSignal = useSignal(initData.state);
const error = ref<string | null>(null);
const debugLogs = ref<string[]>([]);

Expand All @@ -38,12 +38,6 @@ export const useTMAStore = defineStore('tmaStore', () => {
const params = retrieveLaunchParams();
launchParams.value = params;

// Get init data using the signal
const initDataSignal = useSignal(initData.state);
if (initDataSignal.value) {
initDataRef.value = initDataSignal.value as unknown as InitData<RetrieveLaunchParamsError>;
}

isInitialized.value = true;
error.value = null;
} catch (err) {
Expand All @@ -66,7 +60,7 @@ export const useTMAStore = defineStore('tmaStore', () => {
),
isInitialized,
launchParams,
initData: initDataRef,
initData: initDataSignal,
error,
debugLogs,
initialize,
Expand Down
148 changes: 77 additions & 71 deletions packages/frontend-main/src/views/AboutView.vue
Original file line number Diff line number Diff line change
@@ -1,87 +1,93 @@
<script setup lang="ts">
import { ExternalLink } from 'lucide-vue-next';
import { ArrowUpRight } from 'lucide-vue-next';

import TMAInfo from '@/components/tma/TMAInfo.vue';
import Button from '@/components/ui/button/Button.vue';
import { useSEO } from '@/composables/useSEO';
import { externalLinks } from '@/config/externalLinks';
import MainLayout from '@/layouts/MainLayout.vue';

import ViewHeading from './ViewHeading.vue';

interface ActionItem {
id: string;
labelKey: string;
url: string;
}

const actions: ActionItem[] = [
{
id: 'docs',
labelKey: 'components.About.docs',
url: 'https://docs.dither.chat',
},
{
id: 'privacy-policy',
labelKey: 'components.About.privacyPolicy',
url: 'https://docs.dither.chat/policies/privacy-policy',
},
{
id: 'terms-and-conditions',
labelKey: 'components.About.termsAndConditions',
url: 'https://docs.dither.chat/policies/terms-of-service',
},
];

function handleActionClick(url: string) {
window.open(url, '_blank');
}
useSEO({
title: 'About Dither - Open Blockchain Messaging Protocol | dither.chat',
description: 'Dither is an open messaging protocol built on blockchain. Decentralized social messaging, onchain posts, and durable protocol for Web3. Built on AtomOne blockchain.',
ogTitle: 'About Dither - Open Blockchain Messaging Protocol',
ogDescription: 'Open messaging protocol built on blockchain. Decentralized social messaging, onchain posts, and durable protocol for Web3.',
ogUrl: `${externalLinks.siteUrl}/about`,
});
</script>

<template>
<MainLayout>
<div class="flex flex-col h-[calc(100dvh-2*var(--mobile-panel-height))]">
<ViewHeading :title="$t('components.Headings.about')" />
<div class="flex flex-col min-h-full">
<div class="flex flex-col items-center justify-center gap-12 px-6 py-20 max-w-3xl mx-auto w-full min-h-[calc(100vh-220px)] text-center">
<h1 class="text-7xl font-bold tracking-tight leading-none">
dither
</h1>

<div class="flex flex-col flex-1">
<div v-for="action in actions" :key="action.id" class="border-b">
<Button
size="sm"
class="w-full text-left py-8 group"
variant="ghost"
@click="handleActionClick(action.url)"
>
<span class="grow pl-2">
{{ $t(action.labelKey) }}
</span>
<ExternalLink
:size="16"
class="opacity-0 group-hover:opacity-100 transition-opacity duration-200 ease-in-out mr-2"
/>
</Button>
<div class="flex flex-col gap-4 max-w-2xl">
<p class="text-3xl text-foreground font-semibold leading-snug tracking-tight">
{{ $t('components.About.heroTagline') }}
</p>
<p class="text-base text-muted-foreground leading-relaxed">
Posts are stored permanently on
<a
:href="externalLinks.atomoneWebsite"
target="_blank"
rel="noopener noreferrer"
class="text-foreground hover:underline transition-colors"
>
{{ $t('components.About.atomoneLink') }}
</a>.
</p>
<p class="text-base text-muted-foreground/75">
{{ $t('components.About.coreDesc') }}
</p>
</div>

<!-- TMA Info -->
<TMAInfo />

<div
class="flex flex-col justify-end items-center gap-8 px-5 py-8 flex-1"
<a
:href="externalLinks.protocolDocs"
target="_blank"
rel="noopener noreferrer"
class="text-sm text-muted-foreground/80 hover:text-foreground transition-colors flex items-center gap-1"
>
<!-- Aib logo -->
<svg
viewBox="0 0 80 53"
width="120"
height="80"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M24.9998 19.9998 30 30H0L5.00021 19.9998H24.9998zM15 0l4.9998 9.99979H10.0002L15 0zM41.6667.0c2.7614.0 5 2.23858 5 5s-2.2386 5-5 5c-2.7615.0-5-2.23858-5-5s2.2385-5 5-5zm-5 30V20h10V30h-10zM75 20H56.6667V30H75C77.7614 30 80 27.7614 80 25S77.7614 20 75 20zM56.6667.0H68.3333c2.76150000000001.0 5 2.23858 5 5S71.0948 10 68.3333 10H56.6667V0zM77.5939 43.8512C77.9255 44.0839 78.1137 44.4092 78.1583 44.827H79.866C79.8564 44.2753 79.7018 43.7874 79.402 43.3633 79.1022 42.9359 78.6893 42.6027 78.1631 42.3635 77.6401 42.1212 77.0311 42 76.3359 42 75.6503 42 75.0364 42.1212 74.4943 42.3635 73.9522 42.6027 73.5233 42.9391 73.2076 43.3728 72.895 43.8065 72.7388 44.312 72.7388 44.8891 72.7388 45.5907 72.9716 46.1535 73.4372 46.5777 73.9059 46.9986 74.5437 47.3127 75.3505 47.52L76.465 47.807C76.8158 47.8963 77.1219 47.9983 77.3834 48.1131 77.6481 48.2247 77.8538 48.3651 78.0005 48.5341 78.1472 48.6999 78.2221 48.9104 78.2253 49.1655 78.2221 49.4461 78.1376 49.6916 77.9718 49.9021 77.806 50.1094 77.5764 50.272 77.283 50.39 76.9928 50.5048 76.6548 50.5622 76.2689 50.5622 75.899 50.5622 75.5642 50.5064 75.2644 50.3948 74.9678 50.2832 74.7271 50.1142 74.5421 49.8878 74.3572 49.6613 74.2519 49.3775 74.2264 49.0363H72.4805C72.506 49.6869 72.675 50.2385 72.9875 50.6914 73.3032 51.1442 73.7401 51.4886 74.2982 51.7246 74.8594 51.9605 75.5211 52.0785 76.2832 52.0785 77.0677 52.0785 77.7358 51.9573 78.2875 51.715 78.8423 51.4694 79.2665 51.1298 79.5599 50.6961 79.8532 50.2593 79.9999 49.7538 79.9999 49.1798 79.9999 48.7557 79.9202 48.3858 79.7607 48.0701 79.6013 47.7544 79.3845 47.4849 79.1102 47.2617 78.8392 47.0385 78.5314 46.8535 78.187 46.7068 77.8458 46.5601 77.4903 46.4437 77.1203 46.3576L76.2019 46.128C76.001 46.0802 75.8017 46.0212 75.604 45.951 75.4063 45.8809 75.2261 45.7948 75.0635 45.6927 74.9009 45.5875 74.7717 45.46 74.676 45.3101 74.5836 45.1602 74.5373 44.9832 74.5373 44.7791 74.5405 44.5336 74.6123 44.3151 74.7526 44.1238 74.8929 43.9325 75.0954 43.781 75.3601 43.6694 75.6247 43.5578 75.942 43.502 76.312 43.502 76.8381 43.502 77.2654 43.6184 77.5939 43.8512zM62.973 42.1339v1.4877h3.0279v8.3087h1.7603V43.6216H70.789V42.1339H62.973zm-2.03.0V51.9303H59.1684V42.1339H60.943zm-11.1662.0V51.9303h3.9654C54.4916 51.9303 55.1134 51.8139 55.6077 51.5811 56.102 51.3483 56.4719 51.031 56.7174 50.6292 56.963 50.2242 57.0857 49.7682 57.0857 49.2611 57.0857 48.7637 56.9837 48.3396 56.7796 47.9888 56.5755 47.638 56.3124 47.3669 55.9903 47.1756 55.6714 46.9811 55.3382 46.8743 54.9906 46.8551V46.7595C55.3095 46.6829 55.5981 46.557 55.8564 46.3816 56.1147 46.2062 56.3204 45.9782 56.4735 45.6975 56.6265 45.4137 56.7031 45.0725 56.7031 44.6739 56.7031 44.1892 56.5851 43.7555 56.3491 43.3728 56.1163 42.9902 55.7639 42.6888 55.292 42.4688 54.8232 42.2456 54.2348 42.1339 53.5269 42.1339H49.7768zM53.4599 50.4474H51.5514v-2.87h1.9564C53.8745 47.5774 54.1902 47.646 54.4549 47.7831 54.7196 47.917 54.9236 48.102 55.0672 48.338 55.2107 48.5708 55.2824 48.8322 55.2824 49.1224 55.2824 49.5115 55.1389 49.8304 54.8519 50.0791 54.5681 50.3246 54.1041 50.4474 53.4599 50.4474zM53.3069 46.3003H51.5514V43.5977h1.7937C53.8681 43.5977 54.262 43.7204 54.5266 43.966 54.7945 44.2115 54.9284 44.524 54.9284 44.9035 54.9284 45.1905 54.8567 45.4392 54.7132 45.6497 54.5729 45.857 54.3799 46.018 54.1344 46.1328 53.8889 46.2444 53.613 46.3003 53.3069 46.3003zm-9.8827-4.1664V51.9303H41.8457l-4.6159-6.6728H37.1484v6.6728H35.3738V42.1339h1.5881L41.5731 48.8115H41.6592V42.1339h1.765zM32.9133 51.9303V42.1339H31.1386V51.9303h1.7747zm-13.8228.0V42.1339h1.7747v8.3087h4.3145v1.4877H19.0905zM10.9222 42.1339V51.9303h6.0892V50.4426H12.6969V42.1339H10.9222zM1.89421 51.9303H0L3.4488 42.1339H5.63958L9.09316 51.9303H7.19895L6.38732 49.5147H2.70435L1.89421 51.9303zM4.58245 44.143l1.32593 3.9462H3.18242l1.3235-3.9462h.07653z"
fill="currentcolor"
/>
</svg>
{{ $t('components.About.ctaDocs') }}
<ArrowUpRight class="size-3" />
</a>

<span class="text-sm">&copy; All in Bits 2025</span>
<div class="flex flex-wrap items-center justify-center gap-4 text-sm text-muted-foreground/80 mt-12 border-t border-border/40 pt-5 w-full">
<a
:href="externalLinks.privacyPolicy"
target="_blank"
rel="noopener noreferrer"
class="hover:text-foreground transition-colors"
>
{{ $t('components.About.ctaPrivacy') }}
</a>
<span aria-hidden="true">•</span>
<a
:href="externalLinks.termsOfService"
target="_blank"
rel="noopener noreferrer"
class="hover:text-foreground transition-colors"
>
{{ $t('components.About.ctaTerms') }}
</a>
<span aria-hidden="true">•</span>
<a
:href="externalLinks.githubRepo"
target="_blank"
rel="noopener noreferrer"
class="hover:text-foreground transition-colors"
>
{{ $t('components.About.ctaGitHub') }}
</a>
<span aria-hidden="true">•</span>
<a
:href="externalLinks.telegramBot"
target="_blank"
rel="noopener noreferrer"
class="hover:text-foreground transition-colors"
>
{{ $t('components.About.ctaTelegram') }}
</a>
</div>
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions packages/frontend-main/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@ export default defineConfig({
),
},
},
optimizeDeps: {
include: ['@sinclair/typebox', 'drizzle-typebox'],
},
});
Loading