Add loading screen; add support for showing mentions; add support for mentioning.
Some checks failed
Build Vite / build (24.x) (push) Has been cancelled
Some checks failed
Build Vite / build (24.x) (push) Has been cancelled
This commit is contained in:
parent
f87966aa15
commit
75ef93ed80
8 changed files with 248 additions and 94 deletions
15
web/app.html
15
web/app.html
|
|
@ -14,6 +14,19 @@
|
||||||
<link rel="manifest" href="./manifest.json">
|
<link rel="manifest" href="./manifest.json">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="loading-screen">
|
||||||
|
<h1>Pandora</h1>
|
||||||
|
<p id="loading-screen-status">Please wait...</p>
|
||||||
|
<div class='loading-animation-wrapper'>
|
||||||
|
<div class="animation">
|
||||||
|
<div class="dot"></div>
|
||||||
|
<div class="dot"></div>
|
||||||
|
<div class="dot"></div>
|
||||||
|
<div class="dot"></div>
|
||||||
|
<div class="dot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="app-container no-active-chat">
|
<div class="app-container no-active-chat">
|
||||||
<div class="main-holder">
|
<div class="main-holder">
|
||||||
<aside id="desktop-aside">
|
<aside id="desktop-aside">
|
||||||
|
|
@ -93,6 +106,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Input Panel -->
|
<!-- Input Panel -->
|
||||||
|
<div id="mentioning-indicator" class="collapsed">
|
||||||
|
</div>
|
||||||
<footer id="chat-input-panel" class="chat-input-panel">
|
<footer id="chat-input-panel" class="chat-input-panel">
|
||||||
<div class="input-actions-left">
|
<div class="input-actions-left">
|
||||||
<button id="chat-bottom-bar-btn" class="chat-footer-btn icon-btn mif-expand-less mif-3x" title="Expand"></button>
|
<button id="chat-bottom-bar-btn" class="chat-footer-btn icon-btn mif-expand-less mif-3x" title="Expand"></button>
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,7 @@ import { fetchChats, getAppUser, getChatMessages, getChatPicture, getChats, getU
|
||||||
import { deleteDatabase, upsertMessages } from "./db";
|
import { deleteDatabase, upsertMessages } from "./db";
|
||||||
import { showNotification } from "./notification";
|
import { showNotification } from "./notification";
|
||||||
import type { Chat, Message, WebSocketEvent } from "./types";
|
import type { Chat, Message, WebSocketEvent } from "./types";
|
||||||
import { activeChatState, setActiveChatState } from "./states";
|
import { activeChatState, clearMentionCache, mentionCacheID, mentionCacheText, setActiveChatState } from "./states";
|
||||||
|
|
||||||
if (localStorage.getItem('setupComplete') !== "true") window.location.href = "index.html";
|
if (localStorage.getItem('setupComplete') !== "true") window.location.href = "index.html";
|
||||||
|
|
||||||
const messageTone = new Audio("./message.ogg");
|
const messageTone = new Audio("./message.ogg");
|
||||||
|
|
@ -20,29 +19,41 @@ if (!mainViewEl) throw console.error();
|
||||||
const mainView = views.get(mainViewEl);
|
const mainView = views.get(mainViewEl);
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
updateSidebarPosition();
|
ui.load(async () => {
|
||||||
askForNotificationPermission();
|
ui.loadingMessage("Drawing sidebar...");
|
||||||
if (elements.inputApiKey) elements.inputApiKey.value = config.apiKey;
|
updateSidebarPosition();
|
||||||
if (elements.inputWahaUrl) elements.inputWahaUrl.value = config.wahaUrl;
|
ui.loadingMessage("Asking for notification permission...");
|
||||||
if (elements.inputSession) elements.inputSession.value = config.session;
|
askForNotificationPermission();
|
||||||
if (elements.inputBackgroundImage) elements.inputBackgroundImage.value = config.bgImg;
|
ui.loadingMessage("Loading configuration...");
|
||||||
if (elements.inputBackgroundOpacity) elements.inputBackgroundOpacity.value = config.bgOpacity;
|
if (elements.inputApiKey) elements.inputApiKey.value = config.apiKey;
|
||||||
if (elements.activeChatContainer) {
|
if (elements.inputWahaUrl) elements.inputWahaUrl.value = config.wahaUrl;
|
||||||
elements.activeChatContainer.style.setProperty('--background-image', `URL("${config.bgImg}")`);
|
if (elements.inputSession) elements.inputSession.value = config.session;
|
||||||
elements.activeChatContainer.style.setProperty('--background-opacity', `${config.bgOpacity}`);
|
if (elements.inputBackgroundImage) elements.inputBackgroundImage.value = config.bgImg;
|
||||||
}
|
if (elements.inputBackgroundOpacity) elements.inputBackgroundOpacity.value = config.bgOpacity;
|
||||||
await updateOnlineStatus();
|
if (elements.activeChatContainer) {
|
||||||
setupEventListeners();
|
elements.activeChatContainer.style.setProperty('--background-image', `URL("${config.bgImg}")`);
|
||||||
try {
|
elements.activeChatContainer.style.setProperty('--background-opacity', `${config.bgOpacity}`);
|
||||||
setupElementsData();
|
}
|
||||||
loadChats();
|
ui.loadingMessage("Getting server version...");
|
||||||
checkWahaStatus();
|
await updateOnlineStatus();
|
||||||
initWebSocket();
|
ui.loadingMessage("Setting up event listeners...");
|
||||||
} finally {
|
setupEventListeners();
|
||||||
elements.chatsLoader.classList.add('hidden');
|
try {
|
||||||
}
|
ui.loadingMessage("Replacing placeholders...");
|
||||||
|
await setupElementsData();
|
||||||
|
ui.loadingMessage("Loading chats...");
|
||||||
|
await loadChats();
|
||||||
|
ui.loadingMessage("Checking server status...");
|
||||||
|
await checkWahaStatus();
|
||||||
|
ui.loadingMessage("Telling server to send new messages...");
|
||||||
|
await initWebSocket();
|
||||||
|
} finally {
|
||||||
|
elements.chatsLoader.classList.add('hidden');
|
||||||
|
}
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
async function askForNotificationPermission() {
|
async function askForNotificationPermission() {
|
||||||
notificationAuthorization = await Notification.requestPermission();
|
notificationAuthorization = await Notification.requestPermission();
|
||||||
}
|
}
|
||||||
|
|
@ -199,12 +210,21 @@ function setupEventListeners() {
|
||||||
sendMessage();
|
sendMessage();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
elements.messageInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
elements.chatBottomBar.style.height = `${elements.chatInputPanel.offsetHeight}px`;
|
elements.chatBottomBar.style.height = `${elements.chatInputPanel.offsetHeight}px`;
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
elements.chatBottomBar.style.height =
|
elements.chatBottomBar.style.height =
|
||||||
`${elements.chatInputPanel.offsetHeight}px`;
|
`${elements.chatInputPanel.offsetHeight}px`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
elements.mentioningIndicator.addEventListener('click', clearMentionCache);
|
||||||
|
|
||||||
observer.observe(elements.chatInputPanel);
|
observer.observe(elements.chatInputPanel);
|
||||||
elements.chatBottomBarBtn.addEventListener('click', ui.toggleChatBottomBar);
|
elements.chatBottomBarBtn.addEventListener('click', ui.toggleChatBottomBar);
|
||||||
elements.chatBottomBar.addEventListener('click', (e) => {
|
elements.chatBottomBar.addEventListener('click', (e) => {
|
||||||
|
|
@ -339,6 +359,8 @@ async function handleIncomingMessage(msg: Message) {
|
||||||
async function selectChat(chat: Chat, isPopState = false, smoothScroll = true) {
|
async function selectChat(chat: Chat, isPopState = false, smoothScroll = true) {
|
||||||
if (isLoadingChat) return;
|
if (isLoadingChat) return;
|
||||||
|
|
||||||
|
clearMentionCache();
|
||||||
|
|
||||||
const pageEl = document.getElementById("chat-page");
|
const pageEl = document.getElementById("chat-page");
|
||||||
if (!pageEl) return;
|
if (!pageEl) return;
|
||||||
mainView?.scrollTo(pageEl);
|
mainView?.scrollTo(pageEl);
|
||||||
|
|
@ -416,7 +438,10 @@ async function sendMessage() {
|
||||||
fromMe: true,
|
fromMe: true,
|
||||||
sender: 'me',
|
sender: 'me',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
status: 'sending'
|
status: 'sending',
|
||||||
|
replyTo: mentionCacheID ? {
|
||||||
|
body: mentionCacheText || "Mention (no text)"
|
||||||
|
} : null
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
ui.appendSingleMessage(tempMsg, activeChatState.name, (await getAppUser()).id);
|
ui.appendSingleMessage(tempMsg, activeChatState.name, (await getAppUser()).id);
|
||||||
|
|
@ -445,7 +470,7 @@ async function sendMessage() {
|
||||||
console.warn('readChat failed (non-fatal):', e.message);
|
console.warn('readChat failed (non-fatal):', e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseData = await waha.sendTextMessage(activeChatState.id, text);
|
const responseData = await waha.sendTextMessage(activeChatState.id, text, mentionCacheID);
|
||||||
|
|
||||||
const tempBubble = document.getElementById(tempMsg.id);
|
const tempBubble = document.getElementById(tempMsg.id);
|
||||||
if (tempBubble) {
|
if (tempBubble) {
|
||||||
|
|
@ -466,6 +491,8 @@ async function sendMessage() {
|
||||||
if (meta) meta.innerHTML = `<span style="color: #ef4444;">Failed to send</span>`;
|
if (meta) meta.innerHTML = `<span style="color: #ef4444;">Failed to send</span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearMentionCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendFileMessage(file: File) {
|
async function sendFileMessage(file: File) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,24 @@
|
||||||
import { Chat } from "./types";
|
import { Chat } from "./types";
|
||||||
|
import { elements } from "./ui";
|
||||||
|
|
||||||
export let activeChatState: Chat | null = null;
|
export let activeChatState: Chat | null = null;
|
||||||
|
export let mentionCacheID: string | null = null;
|
||||||
|
export let mentionCacheText: string | null = null;
|
||||||
|
|
||||||
export function setActiveChatState(value: Chat | null) {
|
export function setActiveChatState(value: Chat | null) {
|
||||||
activeChatState = value;
|
activeChatState = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function prepareMention(id: string, text: string) {
|
||||||
|
mentionCacheID = id;
|
||||||
|
mentionCacheText = text;
|
||||||
|
elements.mentioningIndicator.innerText = `Mentioning "${text}".`;
|
||||||
|
elements.mentioningIndicator.classList.remove('collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearMentionCache() {
|
||||||
|
mentionCacheID = null;
|
||||||
|
mentionCacheText = null;
|
||||||
|
elements.mentioningIndicator.innerText = ``;
|
||||||
|
elements.mentioningIndicator.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,7 @@ export interface Message {
|
||||||
chatId?: string;
|
chatId?: string;
|
||||||
chat?: { id: string };
|
chat?: { id: string };
|
||||||
participant?: string;
|
participant?: string;
|
||||||
|
replyTo?: Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Temporary message used for optimistic UI updates */
|
/** Temporary message used for optimistic UI updates */
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { formatTime, normalizeId } from "./utils";
|
import { formatTime, normalizeId } from "./utils";
|
||||||
import { getChatPicture, getMessage, getMedia, getMoreChatMessages } from "./storage";
|
import { getChatPicture, getMessage, getMedia, getMoreChatMessages } from "./storage";
|
||||||
import type { Chat, Message } from "./types";
|
import type { Chat, Message } from "./types";
|
||||||
import { activeChatState } from "./states";
|
import { activeChatState, prepareMention } from "./states";
|
||||||
import { Parser } from "./parser";
|
import { Parser } from "./parser";
|
||||||
|
|
||||||
export const elements = {
|
export const elements = {
|
||||||
|
|
@ -46,7 +46,10 @@ export const elements = {
|
||||||
inputUserStatus: document.getElementById('profile-page-status-input') as HTMLInputElement,
|
inputUserStatus: document.getElementById('profile-page-status-input') as HTMLInputElement,
|
||||||
selectable: document.querySelectorAll('.selectable') as NodeListOf<HTMLElement>,
|
selectable: document.querySelectorAll('.selectable') as NodeListOf<HTMLElement>,
|
||||||
nextPageButtons: document.querySelectorAll('.next-page-btn') as NodeListOf<HTMLElement>,
|
nextPageButtons: document.querySelectorAll('.next-page-btn') as NodeListOf<HTMLElement>,
|
||||||
scrollableViews: document.querySelectorAll('._scrollableView') as NodeListOf<HTMLElement>
|
scrollableViews: document.querySelectorAll('._scrollableView') as NodeListOf<HTMLElement>,
|
||||||
|
loadingScreen: document.querySelector('#loading-screen') as HTMLElement,
|
||||||
|
loadingScreenStatus: document.querySelector('#loading-screen-status') as HTMLElement,
|
||||||
|
mentioningIndicator: document.querySelector('#mentioning-indicator') as HTMLElement
|
||||||
};
|
};
|
||||||
|
|
||||||
export const views = new Map<HTMLElement, ScrollableView>;
|
export const views = new Map<HTMLElement, ScrollableView>;
|
||||||
|
|
@ -276,7 +279,7 @@ export const ui = {
|
||||||
},
|
},
|
||||||
|
|
||||||
generateMessage(msg: Message, userID: string, chatId: string, isLocal: boolean = false) {
|
generateMessage(msg: Message, userID: string, chatId: string, isLocal: boolean = false) {
|
||||||
if (msg._data && msg._data.type == "gp2") return;
|
if (msg._data && msg._data.type == "gp2") return; // probably group description edit
|
||||||
const isOutgoing = msg.fromMe || msg.sender === 'me';
|
const isOutgoing = msg.fromMe || msg.sender === 'me';
|
||||||
|
|
||||||
function getPrevMessageElem() {
|
function getPrevMessageElem() {
|
||||||
|
|
@ -288,6 +291,7 @@ export const ui = {
|
||||||
const groupDiv = document.createElement('div');
|
const groupDiv = document.createElement('div');
|
||||||
groupDiv.className = `message-group selectable ${isOutgoing ? 'outgoing' : 'incoming'}`;
|
groupDiv.className = `message-group selectable ${isOutgoing ? 'outgoing' : 'incoming'}`;
|
||||||
groupDiv.id = normalizeId(msg._serialized ? (msg._serialized as any) : msg.id) || "msg-id";
|
groupDiv.id = normalizeId(msg._serialized ? (msg._serialized as any) : msg.id) || "msg-id";
|
||||||
|
groupDiv.dataset.id = msg.id.toString();
|
||||||
groupDiv.dataset.timestamp = msg.timestamp?.toString();
|
groupDiv.dataset.timestamp = msg.timestamp?.toString();
|
||||||
groupDiv.dataset.from = msg.participant || (msg.from as string);
|
groupDiv.dataset.from = msg.participant || (msg.from as string);
|
||||||
|
|
||||||
|
|
@ -327,6 +331,31 @@ export const ui = {
|
||||||
|
|
||||||
const contentEl = document.createElement('div');
|
const contentEl = document.createElement('div');
|
||||||
contentEl.classList.add('message-content');
|
contentEl.classList.add('message-content');
|
||||||
|
|
||||||
|
if (msg.replyTo) {
|
||||||
|
const replyTo = msg.replyTo;
|
||||||
|
const replyIndicatorEl = document.createElement("div");
|
||||||
|
replyIndicatorEl.classList.add('reply-indicator');
|
||||||
|
replyIndicatorEl.textContent = new Parser(replyTo.body || replyTo.text || "")
|
||||||
|
.parse('_', '<i>$1</i>')
|
||||||
|
.parse('*', '<b>$1</b>')
|
||||||
|
.parse('~', '<s>$1</s>')
|
||||||
|
.parse('```', '<span style="font-family: monospace;">$1</span>')
|
||||||
|
.parse('`', '<code>$1</code>')
|
||||||
|
.replace("\n", "<br>")
|
||||||
|
.input;
|
||||||
|
|
||||||
|
replyIndicatorEl.addEventListener('click', () => {
|
||||||
|
const _msg = document.querySelector(`[id*="${replyTo.id}"]`) as HTMLElement;
|
||||||
|
if (_msg) {
|
||||||
|
_msg.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
this.tempClass(_msg, "mentioned-highlight", 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bubble.appendChild(replyIndicatorEl);
|
||||||
|
}
|
||||||
|
|
||||||
const textEl = document.createElement('div');
|
const textEl = document.createElement('div');
|
||||||
const parsed = new Parser(msg.body || msg.text || "")
|
const parsed = new Parser(msg.body || msg.text || "")
|
||||||
.parse('_', '<i>$1</i>')
|
.parse('_', '<i>$1</i>')
|
||||||
|
|
@ -398,7 +427,6 @@ export const ui = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const meta = document.createElement('div');
|
const meta = document.createElement('div');
|
||||||
meta.className = 'message-meta';
|
meta.className = 'message-meta';
|
||||||
meta.innerHTML = `<span>${timeStr}</span>${statusCheck}`;
|
meta.innerHTML = `<span>${timeStr}</span>${statusCheck}`;
|
||||||
|
|
@ -427,6 +455,10 @@ export const ui = {
|
||||||
} else groupDiv.classList.add('same-sender');
|
} else groupDiv.classList.add('same-sender');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bubble.addEventListener('dblclick', () => {
|
||||||
|
prepareMention(msg.id.toString(), parsed);
|
||||||
|
});
|
||||||
|
|
||||||
groupDiv.appendChild(bubble);
|
groupDiv.appendChild(bubble);
|
||||||
return groupDiv;
|
return groupDiv;
|
||||||
},
|
},
|
||||||
|
|
@ -486,6 +518,25 @@ export const ui = {
|
||||||
});
|
});
|
||||||
observer.observe(element);
|
observer.observe(element);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async load(fn: Function) {
|
||||||
|
this.loadingMessage("Please wait...");
|
||||||
|
elements.loadingScreen.classList.remove('collapsed');
|
||||||
|
await fn();
|
||||||
|
elements.loadingScreen.classList.add('collapsed');
|
||||||
|
this.loadingMessage("Done!");
|
||||||
|
},
|
||||||
|
|
||||||
|
loadingMessage(message: string) {
|
||||||
|
elements.loadingScreenStatus.innerText = message;
|
||||||
|
},
|
||||||
|
|
||||||
|
tempClass(element: HTMLElement, clazz: string, time: number) {
|
||||||
|
element.classList.add(clazz);
|
||||||
|
setTimeout(() => {
|
||||||
|
element.classList.remove(clazz);
|
||||||
|
}, time)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ScrollableView {
|
export class ScrollableView {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
import type { Message, MessageWithTime } from "./types";
|
import type { Message, MessageWithTime } from "./types";
|
||||||
|
|
||||||
/**
|
|
||||||
* Format timestamps (supports Unix epoch seconds/ms, strings and ISO dates)
|
|
||||||
* @param {string|number|Date} dateVal
|
|
||||||
* @returns {string} Formatted HH:MM AM/PM string
|
|
||||||
*/
|
|
||||||
export function formatTime(dateVal: string | number | Date): string {
|
export function formatTime(dateVal: string | number | Date): string {
|
||||||
if (!dateVal) return '';
|
if (!dateVal) return '';
|
||||||
let date: Date;
|
let date: Date;
|
||||||
|
|
@ -26,12 +21,6 @@ export function formatTime(dateVal: string | number | Date): string {
|
||||||
return `${hours}:${minutesStr} ${ampm}`;
|
return `${hours}:${minutesStr} ${ampm}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adjust outgoing messages backdated behind incoming messages due to clock drift.
|
|
||||||
* Uses a 30-second sliding bubble-sort window.
|
|
||||||
* @param {Array} messages List of raw messages from WAHA
|
|
||||||
* @returns {Array} Compensated chronological message array
|
|
||||||
*/
|
|
||||||
export function compensateMessageOrdering(messages: Message[]): Message[] {
|
export function compensateMessageOrdering(messages: Message[]): Message[] {
|
||||||
if (!Array.isArray(messages)) return [];
|
if (!Array.isArray(messages)) return [];
|
||||||
|
|
||||||
|
|
@ -73,11 +62,6 @@ export function getBase64(file: File): Promise<string> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize a WhatsApp ID (chatId, messageId, etc.) to its string representation
|
|
||||||
* @param {string|object} raw
|
|
||||||
* @returns {string|null}
|
|
||||||
*/
|
|
||||||
export function normalizeId(raw: string | { _serialized?: string; user?: string } | null | undefined): string | null {
|
export function normalizeId(raw: string | { _serialized?: string; user?: string } | null | undefined): string | null {
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
if (typeof raw === 'object') {
|
if (typeof raw === 'object') {
|
||||||
|
|
|
||||||
|
|
@ -138,13 +138,14 @@ export const waha = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async sendTextMessage(chatId: string, text: string): Promise<Message> {
|
async sendTextMessage(chatId: string, text: string, replyTo: string | null = null): Promise<Message> {
|
||||||
return request<Message>('/api/sendText', {
|
return request<Message>('/api/sendText', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
chatId,
|
chatId,
|
||||||
text,
|
text,
|
||||||
session: config.session
|
session: config.session,
|
||||||
|
replyTo: replyTo
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
||||||
130
web/style.css
130
web/style.css
|
|
@ -1,31 +1,31 @@
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Selawik;
|
font-family: Selawik;
|
||||||
src: url(./fonts/selawk.ttf);
|
src: url(./fonts/selawk.ttf);
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Selawik;
|
font-family: Selawik;
|
||||||
src: url(./fonts/selawkl.ttf);
|
src: url(./fonts/selawkl.ttf);
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Selawik;
|
font-family: Selawik;
|
||||||
src: url(./fonts/selawksl.ttf);
|
src: url(./fonts/selawksl.ttf);
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Selawik;
|
font-family: Selawik;
|
||||||
src: url(./fonts/selawkb.ttf);
|
src: url(./fonts/selawkb.ttf);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: Selawik;
|
font-family: Selawik;
|
||||||
src: url(./fonts/selawkb.ttf);
|
src: url(./fonts/selawkb.ttf);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|
@ -368,18 +368,18 @@ li {
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-item.selected {
|
.chat-item.selected {
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
transform: scale(0.98) skewX(10deg);
|
transform: scale(0.98) skewX(10deg);
|
||||||
filter: blur(.1px);
|
filter: blur(.1px);
|
||||||
opacity: .6;
|
opacity: .6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.chat-item.selectable:active {
|
.chat-item.selectable:active {
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
transform: scale(0.98) skewX(10deg);
|
transform: scale(0.98) skewX(10deg);
|
||||||
filter: blur(.1px);
|
filter: blur(.1px);
|
||||||
opacity: .6;
|
opacity: .6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-item-info {
|
.chat-item-info {
|
||||||
|
|
@ -578,7 +578,7 @@ li {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: clip;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -658,7 +658,7 @@ li {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
padding-bottom: .6rem;
|
padding-bottom: 0rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
|
|
@ -777,7 +777,7 @@ li {
|
||||||
.chat-input-panel {
|
.chat-input-panel {
|
||||||
padding: .8rem;
|
padding: .8rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
background-color: var(--bg-main);
|
background-color: var(--bg-main);
|
||||||
}
|
}
|
||||||
|
|
@ -833,7 +833,7 @@ li {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -860,15 +860,6 @@ li {
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-bottom-bar-btn {
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#send-button {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-btn {
|
.send-btn {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
|
|
@ -1000,6 +991,73 @@ li {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mentioned-highlight {
|
||||||
|
transform: translateX(2em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mentioned-highlight .message-bubble {
|
||||||
|
background-color: var(--text-primary);
|
||||||
|
color: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.outgoing.mentioned-highlight {
|
||||||
|
transform: translateX(-2em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-indicator {
|
||||||
|
opacity: .7;
|
||||||
|
max-width: 10em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-indicator::before {
|
||||||
|
content: "> ";
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-screen {
|
||||||
|
transition: .8s;
|
||||||
|
height: 100dvh;
|
||||||
|
width: 100dvw;
|
||||||
|
z-index: 999;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: var(--bg-main);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
outline: thick solid var(--bg-secondary);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-screen.collapsed {
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mentioning-indicator {
|
||||||
|
transition: all .4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
max-height: 3em;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 1;
|
||||||
|
margin-top: .8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mentioning-indicator.collapsed {
|
||||||
|
max-height: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.app-container {
|
.app-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue