Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion chat/ChatView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,13 @@
<add-pm-partner ref="addPmPartnerDialog"></add-pm-partner>

<quick-jump ref="quickJump"></quick-jump>

<toast
v-for="t in toasts"
:key="t.id"
v-bind="t"
@dismiss="dismissToast(t.id)"
/>
</div>
</template>

Expand Down Expand Up @@ -366,6 +373,9 @@
import AdLauncherDialog from './ads/AdLauncher.vue';
import Modal from '../components/Modal.vue';
import QuickJump from './QuickJump.vue';
import { ipcRenderer } from 'electron';
import { toasts, showToast, updateToast, dismissToast } from './toast';
import Toast from '../components/Toast.vue';

const unreadClasses = {
[Conversation.UnreadState.None]: '',
Expand All @@ -391,7 +401,8 @@
adCenter: AdCenterDialog,
adLauncher: AdLauncherDialog,
modal: Modal,
'quick-jump': QuickJump
'quick-jump': QuickJump,
toast: Toast
}
})
export default class ChatView extends Vue {
Expand All @@ -405,6 +416,8 @@
focusListener!: () => void;
blurListener!: () => void;
readonly isMac = process.platform === 'darwin';
toasts = toasts;
dismissToast = dismissToast;

channelConversations = core.conversations.channelConversations;
privateConversations = core.conversations.privateConversations;
Expand All @@ -426,6 +439,42 @@
this.mouseButtonListener = (e: MouseEvent) => this.onMouseButton(e);
window.addEventListener('mouseup', this.mouseButtonListener);

ipcRenderer.on(
'auto-backup-status',
(_e, status: string, progress?: number) => {
const id = 'auto-backup';
if (status === 'started') {
showToast({
id,
message: 'Auto backup in progress...',
icon: 'fa-sync',
iconSpin: true,
progress: 0
});
} else if (status === 'progress' && typeof progress === 'number') {
updateToast(id, { progress });
} else if (status === 'success') {
updateToast(id, {
message: 'Auto backup complete',
icon: 'fa-check',
iconSpin: false,
variant: 'success',
progress: 1,
autoDismiss: 5000
});
} else if (status === 'error') {
updateToast(id, {
message: 'Auto backup failed',
icon: 'fa-exclamation-triangle',
iconSpin: false,
variant: 'error',
progress: undefined,
autoDismiss: 5000
});
}
}
);

//We do this because it's a massive pain in the 🫏 to read some monstrosity of
//an if-else statement to compare our platforms and then pick a keyboard shortcut in our keyboard handle event
this.historyNavigateHandleForward = !this.isMac
Expand Down
1 change: 1 addition & 0 deletions chat/locales/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,7 @@
"settings.export.noCharacters": "No character data is available to export.",
"settings.export.saveTitle": "Save Horizon export",
"settings.export.summary": "Exported data for {0} character(s) to {1}.",
"settings.export.manageData": "Manage Data",
"settings.export.title": "Export Horizon data",
"settings.export.zipping": "Finalizing export",
"settings.filter.hideAds": "Hide [b]ads[/b] from matching characters",
Expand Down
1 change: 1 addition & 0 deletions chat/locales/en_uwu.json
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,7 @@
"settings.export.noCharacters": "Sumimasen... No chawacter data is available to export.",
"settings.export.saveTitle": "Save Howizon expowt",
"settings.export.summary": "Expowted data for {0} chawacter(s) to {1}.",
"settings.export.manageData": "Manage Data",
"settings.export.title": "Expowt Howizon data",
"settings.export.zipping": "Finalizing expowt",
"settings.filter.hideAds": "Hide [b]ads[/b] fwom matching chawactews",
Expand Down
70 changes: 70 additions & 0 deletions chat/toast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Vue from 'vue';

export interface ToastEntry {
id: string;
message: string;
icon?: string;
iconSpin?: boolean;
variant?: 'default' | 'error' | 'success';
progress?: number;
autoDismiss?: number;
}

type ToastUpdate = Partial<Omit<ToastEntry, 'id'>>;

const state = Vue.observable({ toasts: [] as ToastEntry[] });

const timers = new Map<string, ReturnType<typeof setTimeout>>();

let nextId = 0;

function scheduleAutoDismiss(id: string, ms: number): void {
clearTimer(id);
if (ms > 0) {
timers.set(
id,
setTimeout(() => dismissToast(id), ms)
);
}
}

function clearTimer(id: string): void {
const t = timers.get(id);
if (t !== undefined) {
clearTimeout(t);
timers.delete(id);
}
}

export function showToast(
opts: Omit<ToastEntry, 'id'> & { id?: string }
): string {
const id = opts.id ?? `toast-${++nextId}`;
const entry: ToastEntry = {
variant: 'default',
autoDismiss: 0,
...opts,
id
};
state.toasts.push(entry);
if (entry.autoDismiss) scheduleAutoDismiss(id, entry.autoDismiss);
return id;
}

export function updateToast(id: string, partial: ToastUpdate): void {
const idx = state.toasts.findIndex(t => t.id === id);
if (idx === -1) return;
const entry = { ...state.toasts[idx], ...partial };
Vue.set(state.toasts, idx, entry);
if (partial.autoDismiss !== undefined) {
scheduleAutoDismiss(id, partial.autoDismiss);
}
}

export function dismissToast(id: string): void {
clearTimer(id);
const idx = state.toasts.findIndex(t => t.id === id);
if (idx !== -1) state.toasts.splice(idx, 1);
}

export const toasts = state.toasts;
95 changes: 95 additions & 0 deletions components/Toast.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<template>
<div class="horizon-toast" :class="'toast-' + variant">
<div class="toast-content">
<span>
<span
v-if="icon"
class="fas fa-fw"
:class="[icon, { 'fa-spin': iconSpin }]"
></span>
{{ message }}
</span>
<a
href="#"
class="toast-dismiss"
@click.prevent="$emit('dismiss')"
aria-label="Dismiss"
>
<span class="fas fa-times"></span>
</a>
</div>
<div
v-if="progress !== undefined"
class="progress"
style="height: 3px; border-radius: 0"
>
<div
class="progress-bar bg-success"
:style="{
width: Math.round(progress * 100) + '%',
transition: 'width 0.3s ease'
}"
></div>
</div>
</div>
</template>

<script setup lang="ts">
withDefaults(
defineProps<{
message: string;
icon?: string;
iconSpin?: boolean;
variant?: 'default' | 'error' | 'success';
progress?: number;
}>(),
{
icon: '',
iconSpin: false,
variant: 'default',
progress: undefined
}
);

defineEmits<{ (e: 'dismiss'): void }>();
</script>

<style lang="scss">
.horizon-toast {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 9999;
background: var(--bs-body-bg, #212529);
color: var(--bs-body-color, #adb5bd);
border: 1px solid var(--bs-border-color, rgba(255, 255, 255, 0.1));
padding: 0;
border-radius: 6px;
font-size: 0.8rem;
min-width: 220px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
opacity: 0.85;
overflow: hidden;

&.toast-error {
border-color: var(--bs-danger, #dc3545);
}

.toast-content {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
}

.toast-dismiss {
margin-left: auto;
color: inherit;
opacity: 0.5;
text-decoration: none;
&:hover {
opacity: 1;
}
}
}
</style>
Loading