746 lines
37 KiB
Vue
746 lines
37 KiB
Vue
<template>
|
|
<div
|
|
:class="{ dark: config?.theme === 'dark' }"
|
|
class="h-screen w-full flex flex-col bg-slate-50 dark:bg-zinc-950 transition-colors"
|
|
>
|
|
<div
|
|
v-if="appInfo?.is_demo"
|
|
class="relative z-[100] bg-blue-600/90 backdrop-blur-sm text-white text-[10px] font-bold uppercase tracking-[0.2em] py-1 text-center select-none border-b border-white/10 shadow-sm"
|
|
>
|
|
Demo Mode • Read Only
|
|
</div>
|
|
|
|
<RouterView v-if="$route.name === 'auth'" />
|
|
|
|
<template v-else>
|
|
<div v-if="isPopoutMode" class="flex flex-1 h-full w-full overflow-hidden bg-slate-50/90 dark:bg-zinc-950">
|
|
<RouterView class="flex-1" />
|
|
</div>
|
|
|
|
<template v-else>
|
|
<!-- header -->
|
|
<div
|
|
class="relative z-[60] flex bg-white/80 dark:bg-zinc-900/70 backdrop-blur border-gray-200 dark:border-zinc-800 border-b min-h-16 shadow-sm transition-colors"
|
|
>
|
|
<div class="flex w-full px-4">
|
|
<button
|
|
type="button"
|
|
class="sm:hidden my-auto mr-4 text-gray-500 hover:text-gray-600 dark:text-gray-400 dark:hover:text-gray-300"
|
|
@click="isSidebarOpen = !isSidebarOpen"
|
|
>
|
|
<MaterialDesignIcon :icon-name="isSidebarOpen ? 'close' : 'menu'" class="size-6" />
|
|
</button>
|
|
<div
|
|
class="hidden sm:flex my-auto w-12 h-12 mr-2 rounded-xl overflow-hidden bg-white/70 dark:bg-white/10 border border-gray-200 dark:border-zinc-700 shadow-inner"
|
|
>
|
|
<img class="w-12 h-12 object-contain p-1.5" :src="logoUrl" />
|
|
</div>
|
|
<div class="my-auto">
|
|
<div
|
|
class="font-semibold cursor-pointer text-gray-900 dark:text-zinc-100 tracking-tight text-lg"
|
|
@click="onAppNameClick"
|
|
>
|
|
{{ $t("app.name") }}
|
|
</div>
|
|
<div class="hidden sm:block text-sm text-gray-600 dark:text-zinc-300">
|
|
{{ $t("app.custom_fork_by") }}
|
|
<a
|
|
target="_blank"
|
|
href="https://github.com/Sudo-Ivan"
|
|
class="text-blue-500 dark:text-blue-300 hover:underline"
|
|
>Sudo-Ivan</a
|
|
>
|
|
</div>
|
|
</div>
|
|
<div class="flex my-auto ml-auto mr-0 sm:mr-2 space-x-2">
|
|
<button
|
|
type="button"
|
|
class="relative rounded-full p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 transition-colors"
|
|
:title="config?.theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
@click="toggleTheme"
|
|
>
|
|
<MaterialDesignIcon
|
|
:icon-name="config?.theme === 'dark' ? 'brightness-6' : 'brightness-4'"
|
|
class="w-6 h-6"
|
|
/>
|
|
</button>
|
|
<LanguageSelector @language-change="onLanguageChange" />
|
|
<NotificationBell />
|
|
<button type="button" class="rounded-full" @click="syncPropagationNode">
|
|
<span
|
|
class="flex text-gray-800 dark:text-zinc-100 bg-white dark:bg-zinc-800/80 border border-gray-200 dark:border-zinc-700 hover:border-blue-400 dark:hover:border-blue-400/60 px-3 py-1.5 rounded-full shadow-sm transition"
|
|
>
|
|
<span :class="{ 'animate-spin': isSyncingPropagationNode }">
|
|
<MaterialDesignIcon icon-name="refresh" class="size-6" />
|
|
</span>
|
|
<span class="hidden sm:inline-block my-auto mx-1 text-sm font-medium">{{
|
|
$t("app.sync_messages")
|
|
}}</span>
|
|
</span>
|
|
</button>
|
|
<button type="button" class="rounded-full" @click="composeNewMessage">
|
|
<span
|
|
class="flex text-white bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500 hover:from-blue-500/90 hover:to-purple-500/90 px-3 py-1.5 rounded-full shadow-md transition"
|
|
>
|
|
<span>
|
|
<MaterialDesignIcon icon-name="email" class="w-6 h-6" />
|
|
</span>
|
|
<span class="hidden sm:inline-block my-auto mx-1 text-sm font-semibold">{{
|
|
$t("app.compose")
|
|
}}</span>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- middle -->
|
|
<div
|
|
ref="middle"
|
|
class="flex flex-1 w-full overflow-hidden bg-slate-50/80 dark:bg-zinc-950 transition-colors"
|
|
>
|
|
<!-- sidebar backdrop for mobile -->
|
|
<div
|
|
v-if="isSidebarOpen"
|
|
class="fixed inset-0 z-[65] bg-black/20 backdrop-blur-sm sm:hidden"
|
|
@click="isSidebarOpen = false"
|
|
></div>
|
|
|
|
<!-- sidebar -->
|
|
<div
|
|
class="fixed inset-y-0 left-0 z-[70] w-72 transform transition-transform duration-300 ease-in-out sm:relative sm:z-0 sm:flex sm:translate-x-0"
|
|
:class="isSidebarOpen ? 'translate-x-0' : '-translate-x-full'"
|
|
>
|
|
<div
|
|
class="flex h-full w-full flex-col overflow-y-auto border-r border-gray-200/70 bg-white dark:border-zinc-800 dark:bg-zinc-900 backdrop-blur"
|
|
>
|
|
<!-- navigation -->
|
|
<div class="flex-1">
|
|
<ul class="py-3 pr-2 space-y-1">
|
|
<!-- messages -->
|
|
<li>
|
|
<SidebarLink :to="{ name: 'messages' }">
|
|
<template #icon>
|
|
<MaterialDesignIcon
|
|
icon-name="message-text"
|
|
class="w-6 h-6 dark:text-white"
|
|
/>
|
|
</template>
|
|
<template #text>
|
|
<span>{{ $t("app.messages") }}</span>
|
|
<span v-if="unreadConversationsCount > 0" class="ml-auto mr-2">{{
|
|
unreadConversationsCount
|
|
}}</span>
|
|
</template>
|
|
</SidebarLink>
|
|
</li>
|
|
|
|
<!-- nomad network -->
|
|
<li>
|
|
<SidebarLink :to="{ name: 'nomadnetwork' }">
|
|
<template #icon>
|
|
<MaterialDesignIcon icon-name="earth" class="w-6 h-6" />
|
|
</template>
|
|
<template #text>{{ $t("app.nomad_network") }}</template>
|
|
</SidebarLink>
|
|
</li>
|
|
|
|
<!-- map -->
|
|
<li>
|
|
<SidebarLink :to="{ name: 'map' }">
|
|
<template #icon>
|
|
<MaterialDesignIcon icon-name="map" class="w-6 h-6" />
|
|
</template>
|
|
<template #text>{{ $t("app.map") }}</template>
|
|
</SidebarLink>
|
|
</li>
|
|
|
|
<!-- archives -->
|
|
<li>
|
|
<SidebarLink :to="{ name: 'archives' }">
|
|
<template #icon>
|
|
<MaterialDesignIcon icon-name="archive" class="w-6 h-6" />
|
|
</template>
|
|
<template #text>{{ $t("app.archives") }}</template>
|
|
</SidebarLink>
|
|
</li>
|
|
|
|
<!-- interfaces -->
|
|
<li>
|
|
<SidebarLink :to="{ name: 'interfaces' }">
|
|
<template #icon>
|
|
<MaterialDesignIcon icon-name="router" class="w-6 h-6" />
|
|
</template>
|
|
<template #text>{{ $t("app.interfaces") }}</template>
|
|
</SidebarLink>
|
|
</li>
|
|
|
|
<!-- network visualiser -->
|
|
<li>
|
|
<SidebarLink :to="{ name: 'network-visualiser' }">
|
|
<template #icon>
|
|
<MaterialDesignIcon icon-name="diagram-projector" class="w-6 h-6" />
|
|
</template>
|
|
<template #text>{{ $t("app.network_visualiser") }}</template>
|
|
</SidebarLink>
|
|
</li>
|
|
|
|
<!-- tools -->
|
|
<li>
|
|
<SidebarLink :to="{ name: 'tools' }">
|
|
<template #icon>
|
|
<MaterialDesignIcon icon-name="wrench" class="size-6" />
|
|
</template>
|
|
<template #text>{{ $t("app.tools") }}</template>
|
|
</SidebarLink>
|
|
</li>
|
|
|
|
<!-- settings -->
|
|
<li>
|
|
<SidebarLink :to="{ name: 'settings' }">
|
|
<template #icon>
|
|
<MaterialDesignIcon icon-name="cog" class="size-6" />
|
|
</template>
|
|
<template #text>{{ $t("app.settings") }}</template>
|
|
</SidebarLink>
|
|
</li>
|
|
|
|
<!-- info -->
|
|
<li>
|
|
<SidebarLink :to="{ name: 'about' }">
|
|
<template #icon>
|
|
<MaterialDesignIcon icon-name="information" class="size-6" />
|
|
</template>
|
|
<template #text>{{ $t("app.about") }}</template>
|
|
</SidebarLink>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div>
|
|
<!-- my identity -->
|
|
<div
|
|
v-if="config"
|
|
class="bg-white/80 border-t dark:border-zinc-800 dark:bg-zinc-900/70 backdrop-blur"
|
|
>
|
|
<div
|
|
class="flex text-gray-700 p-3 cursor-pointer"
|
|
@click="isShowingMyIdentitySection = !isShowingMyIdentitySection"
|
|
>
|
|
<div class="my-auto mr-2">
|
|
<RouterLink :to="{ name: 'profile.icon' }" @click.stop>
|
|
<LxmfUserIcon
|
|
:icon-name="config?.lxmf_user_icon_name"
|
|
:icon-foreground-colour="config?.lxmf_user_icon_foreground_colour"
|
|
:icon-background-colour="config?.lxmf_user_icon_background_colour"
|
|
/>
|
|
</RouterLink>
|
|
</div>
|
|
<div class="my-auto dark:text-white">{{ $t("app.my_identity") }}</div>
|
|
<div class="my-auto ml-auto">
|
|
<button
|
|
type="button"
|
|
class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500"
|
|
@click.stop="saveIdentitySettings"
|
|
>
|
|
{{ $t("common.save") }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="isShowingMyIdentitySection"
|
|
class="divide-y text-gray-900 border-t border-gray-200 dark:text-zinc-200 dark:border-zinc-800"
|
|
>
|
|
<div class="p-2">
|
|
<input
|
|
v-model="displayName"
|
|
type="text"
|
|
:placeholder="$t('app.display_name_placeholder')"
|
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-zinc-800 dark:border-zinc-600 dark:text-zinc-200 dark:focus:ring-blue-400 dark:focus:border-blue-400"
|
|
/>
|
|
</div>
|
|
<div class="p-2 dark:border-zinc-900">
|
|
<div>{{ $t("app.identity_hash") }}</div>
|
|
<div class="text-sm text-gray-700 dark:text-zinc-400">
|
|
{{ config.identity_hash }}
|
|
</div>
|
|
</div>
|
|
<div class="p-2 dark:border-zinc-900">
|
|
<div>{{ $t("app.lxmf_address") }}</div>
|
|
<div class="text-sm text-gray-700 dark:text-zinc-400">
|
|
{{ config.lxmf_address_hash }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- auto announce -->
|
|
<div
|
|
v-if="config"
|
|
class="bg-white/80 border-t dark:bg-zinc-900/70 dark:border-zinc-800"
|
|
>
|
|
<div
|
|
class="flex text-gray-700 p-3 cursor-pointer dark:text-white"
|
|
@click="isShowingAnnounceSection = !isShowingAnnounceSection"
|
|
>
|
|
<div class="my-auto mr-2">
|
|
<MaterialDesignIcon icon-name="radio" class="size-6" />
|
|
</div>
|
|
<div class="my-auto">{{ $t("app.announce") }}</div>
|
|
<div class="ml-auto">
|
|
<button
|
|
type="button"
|
|
class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500"
|
|
@click.stop="sendAnnounce"
|
|
>
|
|
{{ $t("app.announce_now") }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="isShowingAnnounceSection"
|
|
class="divide-y text-gray-900 border-t border-gray-200 dark:text-zinc-200 dark:border-zinc-800"
|
|
>
|
|
<div class="p-2 dark:border-zinc-800">
|
|
<select
|
|
v-model="config.auto_announce_interval_seconds"
|
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-zinc-800 dark:border-zinc-600 dark:text-zinc-200 dark:focus:ring-blue-400 dark:focus:border-blue-400"
|
|
@change="onAnnounceIntervalSecondsChange"
|
|
>
|
|
<option value="0">{{ $t("app.disabled") }}</option>
|
|
<option value="900">Every 15 Minutes</option>
|
|
<option value="1800">Every 30 Minutes</option>
|
|
<option value="3600">Every 1 Hour</option>
|
|
<option value="10800">Every 3 Hours</option>
|
|
<option value="21600">Every 6 Hours</option>
|
|
<option value="43200">Every 12 Hours</option>
|
|
<option value="86400">Every 24 Hours</option>
|
|
</select>
|
|
<div class="text-sm text-gray-700 dark:text-zinc-100">
|
|
<span v-if="config.last_announced_at">{{
|
|
$t("app.last_announced", {
|
|
time: formatSecondsAgo(config.last_announced_at),
|
|
})
|
|
}}</span>
|
|
<span v-else>{{ $t("app.last_announced_never") }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- audio calls -->
|
|
<div
|
|
v-if="config"
|
|
class="bg-white/80 border-t dark:bg-zinc-900/70 dark:border-zinc-800 pb-3"
|
|
>
|
|
<div
|
|
class="flex text-gray-700 p-3 cursor-pointer"
|
|
@click="isShowingCallsSection = !isShowingCallsSection"
|
|
>
|
|
<div class="my-auto mr-2">
|
|
<MaterialDesignIcon icon-name="phone" class="dark:text-white w-6 h-6" />
|
|
</div>
|
|
<div class="my-auto dark:text-white">{{ $t("app.calls") }}</div>
|
|
<div class="ml-auto">
|
|
<RouterLink
|
|
:to="{ name: 'call' }"
|
|
class="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-gray-700 dark:text-zinc-300 transition-colors"
|
|
>
|
|
<MaterialDesignIcon
|
|
icon-name="phone"
|
|
class="w-3.5 h-3.5 flex-shrink-0"
|
|
/>
|
|
</RouterLink>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="isShowingCallsSection"
|
|
class="divide-y text-gray-900 border-t border-gray-200 dark:border-zinc-800"
|
|
>
|
|
<div class="p-2 flex dark:border-zinc-800 dark:text-white">
|
|
<div>
|
|
<div>{{ $t("app.status") }}</div>
|
|
<div class="text-sm text-gray-700 dark:text-white">
|
|
<div v-if="isTelephoneCallActive" class="flex space-x-2">
|
|
<span>{{ $t("app.active_call") }}</span>
|
|
</div>
|
|
<div v-else>{{ $t("app.hung_up_waiting") }}</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="isTelephoneCallActive" class="ml-auto my-auto mr-1 space-x-2">
|
|
<!-- hangup all calls -->
|
|
<button
|
|
:title="$t('app.hangup_all_calls')"
|
|
type="button"
|
|
class="my-auto inline-flex items-center gap-x-1 rounded-full bg-red-500 p-2 text-sm font-semibold text-white shadow-sm hover:bg-red-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
|
|
@click="hangupTelephoneCall"
|
|
>
|
|
<MaterialDesignIcon
|
|
icon-name="phone-hangup"
|
|
class="w-5 h-5 rotate-[135deg] translate-y-0.5"
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="!isPopoutMode" class="flex flex-1 min-w-0 overflow-hidden">
|
|
<RouterView class="flex-1 min-w-0 h-full" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
<CallOverlay v-if="activeCall || isCallEnded" :active-call="activeCall || lastCall" :is-ended="isCallEnded" />
|
|
<Toast />
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import SidebarLink from "./SidebarLink.vue";
|
|
import DialogUtils from "../js/DialogUtils";
|
|
import WebSocketConnection from "../js/WebSocketConnection";
|
|
import GlobalState from "../js/GlobalState";
|
|
import Utils from "../js/Utils";
|
|
import GlobalEmitter from "../js/GlobalEmitter";
|
|
import NotificationUtils from "../js/NotificationUtils";
|
|
import LxmfUserIcon from "./LxmfUserIcon.vue";
|
|
import Toast from "./Toast.vue";
|
|
import ToastUtils from "../js/ToastUtils";
|
|
import MaterialDesignIcon from "./MaterialDesignIcon.vue";
|
|
import NotificationBell from "./NotificationBell.vue";
|
|
import LanguageSelector from "./LanguageSelector.vue";
|
|
import CallOverlay from "./call/CallOverlay.vue";
|
|
import logoUrl from "../assets/images/logo.png";
|
|
|
|
export default {
|
|
name: "App",
|
|
components: {
|
|
LxmfUserIcon,
|
|
SidebarLink,
|
|
Toast,
|
|
MaterialDesignIcon,
|
|
NotificationBell,
|
|
LanguageSelector,
|
|
CallOverlay,
|
|
},
|
|
data() {
|
|
return {
|
|
logoUrl,
|
|
reloadInterval: null,
|
|
appInfoInterval: null,
|
|
|
|
isShowingMyIdentitySection: true,
|
|
isShowingAnnounceSection: true,
|
|
isShowingCallsSection: true,
|
|
|
|
isSidebarOpen: false,
|
|
|
|
displayName: "Anonymous Peer",
|
|
config: null,
|
|
appInfo: null,
|
|
|
|
isTelephoneCallActive: false,
|
|
activeCall: null,
|
|
propagationNodeStatus: null,
|
|
isCallEnded: false,
|
|
lastCall: null,
|
|
endedTimeout: null,
|
|
};
|
|
},
|
|
computed: {
|
|
currentPopoutType() {
|
|
if (this.$route?.meta?.popoutType) {
|
|
return this.$route.meta.popoutType;
|
|
}
|
|
return this.$route?.query?.popout ?? this.getHashPopoutValue();
|
|
},
|
|
isPopoutMode() {
|
|
return this.currentPopoutType != null;
|
|
},
|
|
unreadConversationsCount() {
|
|
return GlobalState.unreadConversationsCount;
|
|
},
|
|
isSyncingPropagationNode() {
|
|
return [
|
|
"path_requested",
|
|
"link_establishing",
|
|
"link_established",
|
|
"request_sent",
|
|
"receiving",
|
|
"response_received",
|
|
"complete",
|
|
].includes(this.propagationNodeStatus?.state);
|
|
},
|
|
},
|
|
watch: {
|
|
$route() {
|
|
this.isSidebarOpen = false;
|
|
},
|
|
config: {
|
|
handler(newConfig) {
|
|
if (newConfig && newConfig.language) {
|
|
this.$i18n.locale = newConfig.language;
|
|
}
|
|
},
|
|
deep: true,
|
|
},
|
|
},
|
|
beforeUnmount() {
|
|
clearInterval(this.reloadInterval);
|
|
clearInterval(this.appInfoInterval);
|
|
if (this.endedTimeout) clearTimeout(this.endedTimeout);
|
|
|
|
// stop listening for websocket messages
|
|
WebSocketConnection.off("message", this.onWebsocketMessage);
|
|
},
|
|
mounted() {
|
|
// listen for websocket messages
|
|
WebSocketConnection.on("message", this.onWebsocketMessage);
|
|
|
|
this.getAppInfo();
|
|
this.getConfig();
|
|
this.updateTelephoneStatus();
|
|
this.updatePropagationNodeStatus();
|
|
|
|
// update info every few seconds
|
|
this.reloadInterval = setInterval(() => {
|
|
this.updateTelephoneStatus();
|
|
this.updatePropagationNodeStatus();
|
|
}, 1000);
|
|
this.appInfoInterval = setInterval(() => {
|
|
this.getAppInfo();
|
|
}, 15000);
|
|
},
|
|
methods: {
|
|
getHashPopoutValue() {
|
|
const hash = window.location.hash || "";
|
|
const match = hash.match(/popout=([^&]+)/);
|
|
return match ? decodeURIComponent(match[1]) : null;
|
|
},
|
|
async onWebsocketMessage(message) {
|
|
const json = JSON.parse(message.data);
|
|
switch (json.type) {
|
|
case "config": {
|
|
this.config = json.config;
|
|
this.displayName = json.config.display_name;
|
|
break;
|
|
}
|
|
case "announced": {
|
|
// we just announced, update config so we can show the new last updated at
|
|
this.getConfig();
|
|
break;
|
|
}
|
|
case "telephone_ringing": {
|
|
NotificationUtils.showIncomingCallNotification();
|
|
this.updateTelephoneStatus();
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
async getAppInfo() {
|
|
try {
|
|
const response = await window.axios.get(`/api/v1/app/info`);
|
|
this.appInfo = response.data.app_info;
|
|
} catch (e) {
|
|
// do nothing if failed to load app info
|
|
console.log(e);
|
|
}
|
|
},
|
|
async getConfig() {
|
|
try {
|
|
const response = await window.axios.get(`/api/v1/config`);
|
|
this.config = response.data.config;
|
|
} catch (e) {
|
|
// do nothing if failed to load config
|
|
console.log(e);
|
|
}
|
|
},
|
|
async sendAnnounce() {
|
|
try {
|
|
await window.axios.get(`/api/v1/announce`);
|
|
} catch (e) {
|
|
ToastUtils.error("failed to announce");
|
|
console.log(e);
|
|
}
|
|
|
|
// fetch config so it updates last announced timestamp
|
|
await this.getConfig();
|
|
},
|
|
async updateConfig(config) {
|
|
// update local state immediately if in demo mode, as websocket is not available
|
|
if (this.appInfo?.is_demo) {
|
|
this.config = {
|
|
...this.config,
|
|
...config,
|
|
};
|
|
return;
|
|
}
|
|
|
|
try {
|
|
WebSocketConnection.send(
|
|
JSON.stringify({
|
|
type: "config.set",
|
|
config: config,
|
|
})
|
|
);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
},
|
|
async saveIdentitySettings() {
|
|
await this.updateConfig({
|
|
display_name: this.displayName,
|
|
});
|
|
},
|
|
async onAnnounceIntervalSecondsChange() {
|
|
await this.updateConfig({
|
|
auto_announce_interval_seconds: this.config.auto_announce_interval_seconds,
|
|
});
|
|
},
|
|
async toggleTheme() {
|
|
if (!this.config) {
|
|
return;
|
|
}
|
|
const newTheme = this.config.theme === "dark" ? "light" : "dark";
|
|
await this.updateConfig({
|
|
theme: newTheme,
|
|
});
|
|
},
|
|
async onLanguageChange(langCode) {
|
|
await this.updateConfig({
|
|
language: langCode,
|
|
});
|
|
this.$i18n.locale = langCode;
|
|
},
|
|
async composeNewMessage() {
|
|
// go to messages route
|
|
await this.$router.push({ name: "messages" });
|
|
|
|
// emit global event handled by MessagesPage
|
|
GlobalEmitter.emit("compose-new-message");
|
|
},
|
|
async syncPropagationNode() {
|
|
// ask to stop syncing if already syncing
|
|
if (this.isSyncingPropagationNode) {
|
|
if (await DialogUtils.confirm("Are you sure you want to stop syncing?")) {
|
|
await this.stopSyncingPropagationNode();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// request sync
|
|
try {
|
|
await axios.get("/api/v1/lxmf/propagation-node/sync");
|
|
} catch (e) {
|
|
const errorMessage = e.response?.data?.message ?? "Something went wrong. Try again later.";
|
|
ToastUtils.error(errorMessage);
|
|
return;
|
|
}
|
|
|
|
// update propagation status
|
|
await this.updatePropagationNodeStatus();
|
|
|
|
// wait until sync has finished
|
|
const syncFinishedInterval = setInterval(() => {
|
|
// do nothing if still syncing
|
|
if (this.isSyncingPropagationNode) {
|
|
return;
|
|
}
|
|
|
|
// finished syncing, stop checking
|
|
clearInterval(syncFinishedInterval);
|
|
|
|
// show result
|
|
const status = this.propagationNodeStatus?.state;
|
|
const messagesReceived = this.propagationNodeStatus?.messages_received ?? 0;
|
|
if (status === "complete" || status === "idle") {
|
|
ToastUtils.success(`Sync complete. ${messagesReceived} messages received.`);
|
|
} else {
|
|
ToastUtils.error(`Sync error: ${status}`);
|
|
}
|
|
}, 500);
|
|
},
|
|
async stopSyncingPropagationNode() {
|
|
// stop sync
|
|
try {
|
|
await axios.get("/api/v1/lxmf/propagation-node/stop-sync");
|
|
} catch {
|
|
// do nothing on error
|
|
}
|
|
|
|
// update propagation status
|
|
await this.updatePropagationNodeStatus();
|
|
},
|
|
async updatePropagationNodeStatus() {
|
|
try {
|
|
const response = await axios.get("/api/v1/lxmf/propagation-node/status");
|
|
this.propagationNodeStatus = response.data.propagation_node_status;
|
|
} catch {
|
|
// do nothing on error
|
|
}
|
|
},
|
|
formatSecondsAgo: function (seconds) {
|
|
return Utils.formatSecondsAgo(seconds);
|
|
},
|
|
async updateTelephoneStatus() {
|
|
try {
|
|
// fetch status
|
|
const response = await axios.get("/api/v1/telephone/status");
|
|
const oldCall = this.activeCall;
|
|
|
|
// update ui
|
|
this.activeCall = response.data.active_call;
|
|
this.isTelephoneCallActive = this.activeCall != null;
|
|
|
|
// If call just ended, show ended state for a few seconds
|
|
if (oldCall != null && this.activeCall == null) {
|
|
this.lastCall = oldCall;
|
|
this.isCallEnded = true;
|
|
|
|
if (this.endedTimeout) clearTimeout(this.endedTimeout);
|
|
this.endedTimeout = setTimeout(() => {
|
|
this.isCallEnded = false;
|
|
this.lastCall = null;
|
|
}, 5000);
|
|
} else if (this.activeCall != null) {
|
|
// if a new call starts, clear ended state
|
|
this.isCallEnded = false;
|
|
this.lastCall = null;
|
|
if (this.endedTimeout) clearTimeout(this.endedTimeout);
|
|
}
|
|
} catch {
|
|
// do nothing on error
|
|
}
|
|
},
|
|
async hangupTelephoneCall() {
|
|
// confirm user wants to hang up call
|
|
if (!(await DialogUtils.confirm("Are you sure you want to hang up the current telephone call?"))) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// hangup call
|
|
await axios.get(`/api/v1/telephone/hangup`);
|
|
|
|
// reload status
|
|
await this.updateTelephoneStatus();
|
|
} catch {
|
|
// ignore error hanging up call
|
|
}
|
|
},
|
|
onAppNameClick() {
|
|
// user may be on mobile, and is unable to scroll back to sidebar, so let them tap app name to do it
|
|
this.$refs["middle"].scrollTo({
|
|
top: 0,
|
|
left: 0,
|
|
behavior: "smooth",
|
|
});
|
|
},
|
|
},
|
|
};
|
|
</script>
|