mirror of
https://github.com/ancsemi/Haven.git
synced 2026-11-02 03:54:57 +00:00
649 lines
46 KiB
JavaScript
649 lines
46 KiB
JavaScript
// ═══════════════════════════════════════════════════════════
|
||
// Haven: Main Client Application
|
||
// Features: chat, voice, themes, images, multi-server,
|
||
// notifications, volume sliders, status bar
|
||
// ═══════════════════════════════════════════════════════════
|
||
|
||
import SocketMethods from './modules/app-socket.js?v=4.21.0';
|
||
import SocketChannelMethods from './modules/app-socket-channels.js?v=4.25.8';
|
||
import SocketEventMethods from './modules/app-socket-events.js?v=4.24.33';
|
||
import UIBindMethods from './modules/app-ui.js?v=4.24.10';
|
||
import ComposerMethods from './modules/app-composer.js?v=4.22.22';
|
||
import ComposerListMethods from './modules/app-composer-lists.js?v=4.22.22';
|
||
import ChannelMenuMethods from './modules/app-channel-menu.js?v=4.25.4';
|
||
import VoiceControlMethods from './modules/app-voice-controls.js?v=4.21.0';
|
||
import MediaGalleryMethods from './modules/app-media-gallery.js?v=4.24.32';
|
||
import MessageActionMethods from './modules/app-message-actions.js?v=4.25.10';
|
||
import PipPanelMethods from './modules/app-pip-panels.js?v=4.25.10';
|
||
import PeopleMethods from './modules/app-people.js?v=4.23.11';
|
||
import SettingsMethods from './modules/app-settings.js?v=4.23.9';
|
||
import AdminControlMethods from './modules/app-admin-controls.js?v=4.23.9';
|
||
import AdminReauthMethods from './modules/app-admin-reauth.js?v=4.23.8';
|
||
import ExtensionUpdateMethods from './modules/app-extension-updates.js?v=4.17.18';
|
||
import ServerBarMethods from './modules/app-server-bar.js?v=4.23.9';
|
||
import MobileMethods from './modules/app-mobile.js?v=4.24.24';
|
||
import MediaMethods from './modules/app-media.js?v=4.24.22';
|
||
import ImageZoomMethods from './modules/app-image-zoom.js?v=4.23.12';
|
||
import MediaGridMethods from './modules/app-media-grid.js?v=4.25.6';
|
||
import FeedAnchorMethods from './modules/app-feed-anchor.js?v=4.24.26';
|
||
import AttachmentMethods from './modules/app-attachments.js?v=4.25.6';
|
||
import AvatarEditorMethods from './modules/app-avatar-editor.js?v=4.23.5';
|
||
import AppearanceMethods from './modules/app-appearance.js?v=4.22.9';
|
||
import AccessibilityMethods from './modules/app-accessibility.js?v=4.22.3';
|
||
import SoundMethods from './modules/app-sounds.js?v=4.21.0';
|
||
import EmojiStickerMethods from './modules/app-emoji-stickers.js?v=4.21.0';
|
||
import BotMethods from './modules/app-bots.js?v=4.23.9';
|
||
import ContextMethods from './modules/app-context.js?v=4.24.24';
|
||
import ChannelMethods from './modules/app-channels.js?v=4.25.8';
|
||
import ChannelContextMethods from './modules/app-channel-context.js?v=4.24.9';
|
||
import ChannelOrganizeMethods from './modules/app-channel-organize.js?v=4.25.11';
|
||
import ChannelUnreadMethods from './modules/app-channel-unread.js?v=4.24.32';
|
||
import MessageMethods from './modules/app-messages.js?v=4.25.10';
|
||
import LinkPreviewMethods from './modules/app-link-previews.js?v=4.25.13';
|
||
import MessageToolMethods from './modules/app-message-tools.js?v=4.25.10';
|
||
import MoveToThreadMethods from './modules/app-move-to-thread.js?v=4.24.29';
|
||
import UserMethods from './modules/app-users.js?v=4.25.8';
|
||
import BirthdayMethods from './modules/app-birthdays.js?v=4.23.11';
|
||
import VoiceMethods from './modules/app-voice.js?v=4.24.5';
|
||
import ThemePickerMethods from './modules/app-theme-picker.js?v=4.24.18';
|
||
import PushToMuteMethods from './modules/app-push-to-mute.js?v=4.24.5';
|
||
import ScreenWebcamMethods from './modules/app-screen-webcam.js?v=4.24.20';
|
||
import StreamTileMethods from './modules/app-stream-tiles.js?v=4.23.9';
|
||
import MusicMethods from './modules/app-music.js?v=4.24.19';
|
||
import MusicKeyMethods from './modules/app-music-keys.js?v=4.24.19';
|
||
import UtilityMethods from './modules/app-utilities.js?v=4.23.9';
|
||
import MessageContentMethods from './modules/app-message-content.js?v=4.24.22';
|
||
import EmojiPickerMethods from './modules/app-emoji-picker.js?v=4.25.2';
|
||
import GifPickerMethods from './modules/app-gif-picker.js?v=4.25.6';
|
||
import ThreadMethods from './modules/app-threads.js?v=4.25.10';
|
||
import AdminMethods from './modules/app-admin.js?v=4.24.34';
|
||
import BrandingMethods from './modules/app-branding.js?v=4.21.0';
|
||
import MembersAdminMethods from './modules/app-members-admin.js?v=4.23.7';
|
||
import AutocompleteMethods from './modules/app-autocomplete.js?v=4.21.0';
|
||
import StatusMethods from './modules/app-status.js?v=4.21.0';
|
||
import DiscordImportMethods from './modules/app-discord-import.js?v=4.24.2';
|
||
import RoleMethods from './modules/app-roles.js?v=4.24.15';
|
||
import RoleAssignMethods from './modules/app-role-assign.js?v=4.24.15';
|
||
import ModerationMethods from './modules/app-moderation.js?v=4.24.32';
|
||
import PlatformMethods from './modules/app-platform.js?v=4.24.5';
|
||
import SearchMethods from './modules/app-search.js?v=4.24.32';
|
||
import FerryMethods from './modules/app-ferry.js?v=4.24.10';
|
||
import ForumMethods from './modules/app-forum.js?v=4.25.9';
|
||
import ForumBlogMethods from './modules/app-forum-blog.js?v=4.25.6';
|
||
import ForumVoteMethods from './modules/app-forum-votes.js?v=4.24.3';
|
||
import ForumPictureVoteMethods from './modules/app-forum-picture-votes.js?v=4.24.3';
|
||
import RoleToolMethods from './modules/app-role-tools.js?v=4.25.16';
|
||
import PermMatrixMethods from './modules/app-perm-matrix.js?v=4.25.18';
|
||
import CallMethods from './modules/app-calls.js?v=4.22.24';
|
||
import ScalingMethods from './modules/app-scaling.js?v=4.14.3';
|
||
import GroupMethods from './modules/app-groups.js?v=4.25.5';
|
||
import TemplateMethods from './modules/app-templates.js?v=4.18.7';
|
||
import MemberDefaultsMethods from './modules/app-member-defaults.js?v=4.22.9';
|
||
import MessageHighlightMethods from './modules/app-message-highlights.js?v=4.22.2';
|
||
import MessageGroupingMethods from './modules/message-grouping.js?v=4.25.1';
|
||
import ModCaseMethods from './modules/app-mod-cases.js?v=4.22.14';
|
||
import MasterVolumeMethods from './modules/app-master-volume.js?v=4.23.6';
|
||
import ServerMuteMethods from './modules/app-server-mute.js?v=4.24.24';
|
||
import ForumUnreadMethods from './modules/app-forum-unread.js?v=4.21.2';
|
||
import ThreadUnreadMethods from './modules/app-thread-unread.js?v=4.25.3';
|
||
import ThreadDockMethods from './modules/app-thread-dock.js?v=4.25.7';
|
||
import TypingMethods from './modules/app-typing.js?v=4.25.8';
|
||
import MessageNsfwMethods from './modules/app-message-nsfw.js?v=4.25.10';
|
||
import MediaPlaybackMethods from './modules/app-media-playback.js?v=4.25.13';
|
||
import PermDraftMethods from './modules/app-perm-draft.js?v=4.25.17';
|
||
import ThreadScrollMethods from './modules/app-thread-scroll.js?v=4.24.26';
|
||
import ChannelCategoryMethods from './modules/app-channel-categories.js?v=4.22.2';
|
||
import ChannelPersonalOrderMethods from './modules/app-channel-personal-order.js?v=4.22.23';
|
||
import BadgeMethods from './modules/app-badges.js?v=4.24.15';
|
||
import SilentMethods from './modules/app-silent.js?v=4.24.32';
|
||
|
||
class HavenApp {
|
||
constructor() {
|
||
this.token = localStorage.getItem('haven_token');
|
||
this.user = JSON.parse(localStorage.getItem('haven_user') || 'null');
|
||
this.socket = null;
|
||
this.voice = null;
|
||
this.currentChannel = null;
|
||
this.channels = [];
|
||
this.lastTypingEmit = 0;
|
||
this.unreadCounts = {};
|
||
// Per-channel thread @mention list, persisted to localStorage
|
||
try { this._threadMentions = JSON.parse(localStorage.getItem('haven_thread_mentions') || '{}'); }
|
||
catch { this._threadMentions = {}; }
|
||
this.onlineCount = 0;
|
||
this.pingInterval = null;
|
||
this.serverManager = new ServerManager();
|
||
this.notifications = new NotificationManager();
|
||
this.replyingTo = null; // message object being replied to
|
||
this._threadReplyingTo = null; // thread message being replied to
|
||
this._activeThreadParent = null; // currently open thread parent message ID
|
||
this._lastMoveSelectedEl = null; // last clicked message in move-selection mode
|
||
this._imageQueue = []; // queued images awaiting send
|
||
this.channelMembers = []; // for @mention autocomplete
|
||
this.mentionQuery = ''; // current partial @mention being typed
|
||
this.mentionStart = -1; // cursor position of the '@'
|
||
this.editingMsgId = null; // message currently being edited
|
||
this.serverSettings = {}; // server-wide settings
|
||
this.adminActionTarget = null; // { userId, username, action } for modal
|
||
this.highScores = {}; // { flappy: [{user_id, username, score}] }
|
||
this.userStatus = 'online'; // current user's status
|
||
this.userStatusText = ''; // custom status text
|
||
this.idleTimer = null; // auto-away timer
|
||
this.voiceCounts = {}; // { channelCode: count } for sidebar voice indicators
|
||
this.voiceChannelUsers = {}; // { channelCode: [{id, username}] } for sidebar voice user lists
|
||
this.e2e = null; // HavenE2E instance for DM encryption
|
||
this._dmPublicKeys = {}; // { userId → jwk } cache for DM partner public keys
|
||
this._e2eListenersAttached = false;
|
||
this._e2eInitDone = false;
|
||
this._e2eWrappingKey = null; // wrapping key kept in memory for cross-device sync
|
||
this._pendingKeyReqs = {}; // userId → [resolve] for promise-based partner key fetch
|
||
this._pendingE2ENotice = null; // E2E notice text to re-append after message re-render
|
||
this._e2eNoKey = new Set(); // DM partners the server has no public key for
|
||
this._e2eKeyNotices = new Map(); // partner id -> key-change note shown in their DM this session
|
||
this._plainDmOk = new Set(); // DM codes the user agreed to send unencrypted this session
|
||
this._dmGateAsking = new Map(); // DM code -> the send question in progress
|
||
this._oldestMsgId = null; // oldest message ID in current view (for pagination)
|
||
this._noMoreHistory = false; // true when all history has been loaded
|
||
this._loadingHistory = false; // prevent concurrent history requests
|
||
this._historyBefore = null; // set when requesting older messages
|
||
this._nicknames = JSON.parse(localStorage.getItem('haven_nicknames') || '{}'); // client-side nicknames { oderId: name }
|
||
|
||
// Slash command definitions for autocomplete
|
||
this.slashCommands = [
|
||
{ cmd: 'shrug', args: t('commands.args.optional_text'), desc: t('commands.description.shrug') },
|
||
{ cmd: 'tableflip', args: t('commands.args.optional_text'), desc: t('commands.description.tableflip') },
|
||
{ cmd: 'unflip', args: t('commands.args.optional_text'), desc: t('commands.description.unflip') },
|
||
{ cmd: 'lenny', args: t('commands.args.optional_text'), desc: t('commands.description.lenny') },
|
||
{ cmd: 'disapprove', args: t('commands.args.optional_text'), desc: t('commands.description.disapprove') },
|
||
{ cmd: 'me', args: t('commands.args.action'), desc: t('commands.description.me') },
|
||
{ cmd: 'spoiler', args: t('commands.args.text'), desc: t('commands.description.spoiler') },
|
||
{ cmd: 'tts', args: t('commands.args.text'), desc: t('commands.description.tts') },
|
||
{ cmd: 'tts:stop', args: '', desc: t('commands.description.tts_stop') },
|
||
{ cmd: 'break', args: t('commands.args.message'), desc: t('commands.description.break') },
|
||
{ cmd: 'bbs', args: '', desc: t('commands.description.bbs') },
|
||
{ cmd: 'brb', args: '', desc: t('commands.description.brb') },
|
||
{ cmd: 'afk', args: '', desc: t('commands.description.afk') },
|
||
{ cmd: 'boobs', args: '', desc: '( . Y . )' },
|
||
{ cmd: 'butt', args: '', desc: '( . )( . )' },
|
||
{ cmd: 'nick', args: t('commands.args.name'), desc: t('commands.description.nick') },
|
||
{ cmd: 'clear', args: '', desc: t('commands.description.clear') },
|
||
{ cmd: 'flip', args: '', desc: t('commands.description.flip') },
|
||
{ cmd: 'roll', args: '[NdN]', desc: t('commands.description.roll') },
|
||
{ cmd: 'hug', args: '<@user>', desc: t('commands.description.hug') },
|
||
{ cmd: 'wave', args: t('commands.args.optional_text'), desc: t('commands.description.wave') },
|
||
{ cmd: 'play', args: t('commands.args.name_or_url'), desc: t('commands.description.play') },
|
||
{ cmd: 'gif', args: t('commands.args.query'), desc: t('commands.description.gif') },
|
||
{ cmd: 'poll', args: t('commands.args.optional_question'), desc: t('commands.description.poll') },
|
||
{ cmd: 'schedule', args: t('commands.args.optional_text'), desc: t('commands.description.schedule') },
|
||
{ cmd: 'time', args: t('commands.args.time'), desc: t('commands.description.time') },
|
||
];
|
||
|
||
// Load bot-registered slash commands for autocomplete
|
||
this._loadBotCommands();
|
||
|
||
// Emoji palette organized by category
|
||
this.emojiCategories = {
|
||
'Smileys': ['😀','😁','😂','🤣','😃','😄','😅','😆','😉','😊','😋','😎','😍','🥰','😘','🙂','🤗','🤩','🤔','😐','🙄','😏','😣','😥','😮','😯','😴','😛','😜','😝','😒','😔','🙃','😲','😤','😭','😢','😱','🥺','😠','😡','🤬','😈','💀','💩','🤡','👻','😺','😸','🫠','🫣','🫢','🫥','🫤','🥹','🥲','😶🌫️','🤭','🫡','🤫','🤥','😬','🫨','😵','😵💫','🥴','😮💨','😤','🥱','😇','🤠','🤑','🤓','😈','👿','🫶','🤧','😷','🤒','🤕','💅'],
|
||
'People': ['👋','🤚','✋','🖖','👌','🤌','✌️','🤞','🤟','🤘','🤙','👈','👉','👆','👇','☝️','👍','👎','✊','👊','🤛','🤜','👏','🙌','🤝','🙏','💪','🫡','🫶','💅','💃','🕺','🤳','🖕','🫰','🫳','🫴','👐','🤲','🫱','🫲','🤷','🤦','🙇','💁','🙆','🙅','🤷♂️','🤷♀️','🙋','🙋♂️','🙋♀️','🧏','🧑🤝🧑','👫','👬','👭'],
|
||
'Monkeys': ['🙈','🙉','🙊','🐵','🐒','🦍','🦧'],
|
||
'Animals': ['🐶','🐱','🐭','🐹','🐰','🦊','🐻','🐼','🐨','🐯','🦁','🐮','🐷','🐸','🐔','🐧','🐦','🦆','🦅','🦉','🐺','🐴','🦄','🐝','🦋','🐌','🐞','🐢','🐍','🐙','🐬','🐳','🦈','🐊','🦖','🦕','🐋','🦭','🦦','🦫','🦥','🐿️','🦔','🦇','🐓','🦃','🦚','🦜','🦢','🦩','🐕','🐈','🐈⬛'],
|
||
'Faces': ['👀','👁️','👁️🗨️','👅','👄','🫦','💋','🧠','🦷','🦴','👃','👂','🦻','🦶','🦵','💀','☠️','👽','🤖','🎃','😺','😸','😹','😻','😼','😽','🙀','😿','😾'],
|
||
'Food': ['🍎','🍐','🍊','🍋','🍌','🍉','🍇','🍓','🫐','🍒','🍑','🥭','🍍','🥝','🍅','🥑','🌽','🌶️','🫑','🥦','🧄','🧅','🥕','🍕','🍔','🍟','🌭','🍿','🧁','🍩','🍪','🍰','🎂','🧀','🥚','🥓','🥩','🍗','🌮','🌯','🫔','🥙','🍜','🍝','🍣','🍱','☕','🍺','🍻','🍷','🥤','🧊','🧋','🍵','🥂','🍾','🥃','🍶','🫗','🍸','🍹'],
|
||
'Activities':['⚽','🏀','🏈','⚾','🎾','🏐','🎱','🏓','🎮','🕹️','🎲','🧩','🎯','🎳','🎭','🎨','🎼','🎵','🎶','🎸','🥁','🎹','🏆','🥇','🏅','🎪','🎬','🎤','🎧','🎺','🪘','🎻','🪗','🎉','🎊','🎈','🎀','🎗️','🏋️','🤸','🧗','🏄','🏊','🚴','⛷️','🏂','🤺'],
|
||
'Travel': ['🚗','🚕','🚀','✈️','🚁','🛸','🚢','🏠','🏢','🏰','🗼','🗽','⛩️','🌋','🏔️','🌊','🌅','🌄','🌉','🎡','🎢','🗺️','🧭','🏖️','🏕️','🌍','🌎','🌏','🛳️','⛵','🚂','🚇','🏎️','🏍️','🛵','🛶'],
|
||
'Objects': ['⌚','📱','💻','⌨️','🖥️','💾','📷','🔭','🔬','💡','🔦','📚','📝','✏️','📎','📌','🔑','🔒','🔓','🛡️','⚔️','🔧','💰','💎','📦','🎁','✉️','🔔','🪙','💸','🏷️','🔨','🪛','🧲','🧪','🧫','💊','🩺','🩹','🧬','💬','💭','🗨️','🗯️','📣','📢','🔊','🔇','📰','🗞️','📋','📁','📂','🗂️','📅','📆','🗓️','🖊️','🖋️','✒️','📏','📐','🗑️','👑','💍','👒','🎩','🧢','👓','🕶️','🧳','🌂','☂️'],
|
||
'Symbols': ['❤️','🧡','💛','💚','💙','💜','🖤','🤍','🤎','💔','❣️','💕','💞','💓','💗','💖','💝','✨','⭐','🌟','💫','🔥','💯','✅','❌','❗','❓','❕','❔','‼️','⁉️','!','?',',','.','💤','🚫','⚠️','♻️','🏳️','🏴','🎵','➕','➖','➗','💲','♾️','🔴','🟠','🟡','🟢','🔵','🟣','⚫','⚪','🟤','🔶','🔷','🔺','🔻','💠','🔘','🏳️🌈','🏴☠️','⚡','☀️','🌙','🌈','☁️','❄️','💨','🌪️','☮️','✝️','☪️','🕉️','☯️','✡️','🔯','♈','♉','♊','♋','♌','♍','♎','♏','♐','♑','♒','♓','⛎','🆔','⚛️','🈶','🈚','🈸','🈺','🈷️','🆚','🉐','🈹','🈲','🉑','🈴','🈳','㊗️','㊙️','🈵','🔅','🔆','🔱','📛','♻️','🔰','⭕','✳️','❇️','🔟','🔠','🔡','🔢','🔣','🔤','🆎','🆑','🆒','🆓','ℹ️','🆕','🆖','🅾️','🆗','🅿️','🆘','🆙','🆚','🈁','🈂️','💱','💲','#️⃣','*️⃣','0️⃣','1️⃣','2️⃣','3️⃣','4️⃣','5️⃣','6️⃣','7️⃣','8️⃣','9️⃣','🔟','©️','®️','™️'],
|
||
'Flags': [':flag_us:',':us_betsy_ross:',':gadsden:',':flag_gb:',':flag_ca:',':flag_au:',':flag_nz:',':flag_ie:',':flag_fr:',':flag_de:',':flag_it:',':flag_es:',':flag_pt:',':flag_nl:',':flag_be:',':flag_lu:',':flag_ch:',':flag_at:',':flag_dk:',':flag_no:',':flag_se:',':flag_fi:',':flag_is:',':flag_pl:',':flag_ee:',':flag_lv:',':flag_lt:',':flag_cz:',':flag_sk:',':flag_hu:',':flag_ro:',':flag_bg:',':flag_si:',':flag_hr:',':flag_gr:',':flag_al:',':flag_me:',':flag_mk:',':flag_ua:',':flag_tr:',':flag_jp:',':flag_kr:',':flag_tw:',':flag_ph:',':flag_th:',':flag_sg:',':flag_in:',':flag_id:',':flag_my:',':flag_vn:',':flag_mn:',':flag_bd:',':flag_lk:',':flag_np:',':flag_il:',':flag_sa:',':flag_ae:',':flag_qa:',':flag_bh:',':flag_kw:',':flag_om:',':flag_jo:',':flag_eg:',':flag_ma:',':flag_tn:',':flag_mx:',':flag_br:',':flag_ar:',':flag_cl:',':flag_co:',':flag_pe:',':flag_uy:',':flag_ec:',':flag_cr:',':flag_pa:',':flag_gt:',':flag_do:',':flag_jm:',':flag_bs:',':flag_tt:',':flag_za:',':flag_ke:',':flag_ng:',':flag_gh:',':flag_sn:',':flag_rw:',':flag_bw:',':flag_ci:',':flag_tz:']
|
||
};
|
||
|
||
// Flat list for quick access (used by search)
|
||
this.emojis = Object.values(this.emojiCategories).flat();
|
||
|
||
// Emoji name map for search (emoji → keywords)
|
||
this.emojiNames = {
|
||
'😀':'grinning happy','😁':'beaming grin','😂':'joy tears laughing lol','🤣':'rofl rolling laughing','😃':'smiley happy','😄':'smile happy','😅':'sweat nervous','😆':'laughing satisfied','😉':'wink','😊':'blush happy shy','😋':'yummy delicious','😎':'cool sunglasses','😍':'heart eyes love','🥰':'loving smiling hearts','😘':'kiss blowing','🙂':'slight smile','🤗':'hug hugging open hands','🤩':'starstruck star eyes','🤔':'thinking hmm','😐':'neutral expressionless','🙄':'eye roll','😏':'smirk','😣':'persevere','😥':'sad relieved disappointed','😮':'open mouth wow surprised','😯':'hushed surprised','😴':'sleeping zzz','😛':'tongue playful','😜':'wink tongue crazy','😝':'squinting tongue','😒':'unamused','😔':'pensive sad','🙃':'upside down','😲':'astonished shocked','😤':'triumph huff angry steam','😭':'crying sob loudly','😢':'cry sad tear','😱':'scream fear horrified','🥺':'pleading puppy eyes please','😠':'angry mad','😡':'rage pouting furious','🤬':'cursing swearing angry','😈':'devil smiling imp','💀':'skull dead','💩':'poop poo','🤡':'clown','👻':'ghost boo','😺':'cat smile','😸':'cat grin','🫠':'melting face','🫣':'peeking eye','🫢':'hand over mouth','🫥':'dotted line face','🫤':'diagonal mouth','🥹':'holding back tears','🥲':'smile tear','😶🌫️':'face in clouds','🤭':'giggling hand over mouth','🫡':'salute','🤫':'shush quiet secret','🤥':'lying pinocchio','😬':'grimace awkward','🫨':'shaking face','😵':'dizzy','😵💫':'face spiral eyes','🥴':'woozy drunk','😮💨':'exhale sigh relief','🥱':'yawn tired boring','😇':'angel innocent halo','🤠':'cowboy yeehaw','🤑':'money face rich','🤓':'nerd glasses','👿':'devil angry imp','🫶':'heart hands','🤧':'sneeze sick','😷':'mask sick','🤒':'thermometer sick','🤕':'bandage hurt','💅':'nail polish sassy',
|
||
'👋':'wave hello hi bye','🤚':'raised back hand','✋':'hand stop high five','🖖':'vulcan spock','👌':'ok okay perfect','🤌':'pinched italian','✌️':'peace victory','🤞':'crossed fingers luck','🤟':'love you hand','🤘':'rock on metal','🤙':'call me shaka hang loose','👈':'point left','👉':'point right','👆':'point up','👇':'point down','☝️':'index up','👍':'thumbs up like good yes','👎':'thumbs down dislike bad no','✊':'fist bump','👊':'punch fist bump','🤛':'left fist bump','🤜':'right fist bump','👏':'clap applause','🙌':'raising hands celebrate','🤝':'handshake deal','🙏':'pray please thank you namaste','💪':'strong muscle flex bicep','💃':'dancer dancing woman','🕺':'man dancing','🤳':'selfie','🖕':'middle finger','🫰':'pinch','🫳':'palm down','🫴':'palm up','👐':'open hands','🤲':'palms up','🫱':'right hand','🫲':'left hand','🤷':'shrug idk','🤦':'facepalm','🙇':'bow','💁':'info','🙆':'ok gesture','🙅':'no gesture','🙋':'raising hand hi','🧏':'deaf',
|
||
'🐶':'dog puppy','🐱':'cat kitty','🐭':'mouse','🐹':'hamster','🐰':'rabbit bunny','🦊':'fox','🐻':'bear','🐼':'panda','🐨':'koala','🐯':'tiger','🦁':'lion','🐮':'cow','🐷':'pig','🐸':'frog','🐔':'chicken','🐧':'penguin','🐦':'bird','🦆':'duck','🦅':'eagle','🦉':'owl','🐺':'wolf','🐴':'horse','🦄':'unicorn','🐝':'bee','🦋':'butterfly','🐌':'snail','🐞':'ladybug','🐢':'turtle','🐍':'snake','🐙':'octopus','🐬':'dolphin','🐳':'whale','🦈':'shark','🐊':'crocodile alligator','🦖':'trex dinosaur','🦕':'dinosaur brontosaurus',
|
||
'🍎':'apple red','🍐':'pear','🍊':'orange tangerine','🍋':'lemon','🍌':'banana','🍉':'watermelon','🍇':'grapes','🍓':'strawberry','🍒':'cherry','🍑':'peach','🍍':'pineapple','🍕':'pizza','🍔':'burger hamburger','🍟':'fries french','🌭':'hotdog','🍿':'popcorn','🧁':'cupcake','🍩':'donut','🍪':'cookie','🍰':'cake','🎂':'birthday cake','🧀':'cheese','🥚':'egg','🥓':'bacon','🌮':'taco','🍜':'noodles ramen','🍝':'spaghetti pasta','🍣':'sushi','☕':'coffee','🍺':'beer','🍻':'clinking beers cheers toast','🍷':'wine','🍾':'champagne','🥂':'clinking glasses cheers toast','🥃':'tumbler whiskey bourbon','🍶':'sake','🫗':'pouring liquid','🍸':'cocktail martini','🍹':'tropical drink',
|
||
'⚽':'soccer football','🏀':'basketball','🏈':'football american','🎮':'gaming controller video game','🕹️':'joystick arcade','🎲':'dice','🧩':'puzzle jigsaw','🎯':'bullseye target dart','🎨':'art palette paint','🎵':'music note','🎶':'music notes melody song','🎸':'guitar','🏆':'trophy winner','🎧':'headphones music','🎤':'microphone karaoke sing','🎉':'party popper celebration tada','🎊':'confetti ball celebrate','🎈':'balloon party','🎀':'ribbon bow','🎗️':'reminder ribbon',
|
||
'🚗':'car automobile','🚀':'rocket space launch','✈️':'airplane plane travel','🏠':'house home','🏰':'castle','🌊':'wave ocean water','🌅':'sunrise','🌍':'globe earth world','🌈':'rainbow',
|
||
'❤️':'red heart love','🧡':'orange heart','💛':'yellow heart','💚':'green heart','💙':'blue heart','💜':'purple heart','🖤':'black heart','🤍':'white heart','💔':'broken heart','✨':'sparkles stars','⭐':'star','🔥':'fire hot lit','💯':'hundred perfect','✅':'check mark yes','❌':'cross mark no wrong','❗':'exclamation mark bang','❓':'question mark','❕':'white exclamation','❔':'white question','‼️':'double exclamation bangbang','⁉️':'exclamation question interrobang','!':'exclamation punctuation bang','?':'question punctuation mark',',':'comma punctuation','.':'period punctuation dot','💤':'sleep zzz','⚠️':'warning caution','⚡':'lightning bolt zap','☀️':'sun sunny','🌙':'moon crescent night','❄️':'snowflake cold winter','🌪️':'tornado','🔴':'red circle','🔵':'blue circle','🟢':'green circle','🟡':'yellow circle','🟠':'orange circle','🟣':'purple circle','⚫':'black circle','⚪':'white circle','©️':'copyright','®️':'registered','™️':'trademark','#️⃣':'hash number sign','*️⃣':'asterisk star keycap',
|
||
'🙈':'see no evil monkey','🙉':'hear no evil monkey','🙊':'speak no evil monkey',
|
||
'👀':'eyes looking','👅':'tongue','👄':'mouth lips','💋':'kiss lips','🧠':'brain smart','🦷':'tooth','🦴':'bone','💀':'skull dead','☠️':'skull crossbones','👽':'alien','🤖':'robot','🎃':'jack o lantern pumpkin halloween',
|
||
'📱':'phone mobile','💻':'laptop computer','📷':'camera photo','📚':'books reading','📝':'memo note write','🔑':'key','🔒':'lock locked','💎':'gem diamond jewel','🎁':'gift present','🔔':'bell notification','💰':'money bag rich','🔨':'hammer tool','💬':'speech bubble chat','💭':'thought bubble thinking','🗨️':'speech balloon','🗯️':'anger bubble','📣':'megaphone announcement','📢':'loudspeaker','👑':'crown king queen royal','💍':'ring diamond wedding','🕶️':'sunglasses cool'
|
||
};
|
||
|
||
if (!this.token || !this.user) {
|
||
// Preserve invite param so it survives the redirect to the auth page
|
||
const _inv = new URLSearchParams(window.location.search).get('invite');
|
||
if (_inv) sessionStorage.setItem('haven_pending_invite', _inv);
|
||
window.location.href = '/';
|
||
return;
|
||
}
|
||
|
||
// Permission helper: true if user is admin or has mod role
|
||
this._canModerate = () => this.user.isAdmin || (this.user.effectiveLevel || 0) >= 25;
|
||
this._isServerMod = () => this.user.isAdmin || (this.user.effectiveLevel || 0) >= 50;
|
||
this._hasPerm = (p) => this.user.isAdmin || (this.user.permissions || []).includes('*') || (this.user.permissions || []).includes(p);
|
||
// Whether Settings should offer the Admin tab at all. One list, shared by
|
||
// the tab switch, the admin panel container and the settings nav, so a
|
||
// permission added in one place cannot be missed in the others. (#5470:
|
||
// invite_users was in the nav but not in the tab gate, so holders saw an
|
||
// Admin tab that did nothing when clicked.)
|
||
// Mirrors the access table in _syncSettingsNav (app-admin.js): anyone who
|
||
// can see at least one admin section must also be allowed onto the tab.
|
||
this._hasAnyAdminSettingsAccess = () => this.user.isAdmin || [
|
||
'manage_emojis', 'manage_stickers', 'manage_soundboard', 'manage_roles',
|
||
'manage_server', 'manage_webhooks', 'view_audit_log', 'invite_users',
|
||
'ban_user', 'kick_user', 'view_all_members', 'manage_badges'
|
||
].some(p => this._hasPerm(p));
|
||
// Global-only variant: excludes permissions granted via a channel-scoped
|
||
// role assignment, for gating UI that always performs a server-wide
|
||
// action (e.g. the sidebar "Create Channel" section always creates a
|
||
// top-level channel, regardless of which channel is active). (#5433)
|
||
this._hasGlobalPerm = (p) => this.user.isAdmin || (this.user.globalPermissions || []).includes('*') || (this.user.globalPermissions || []).includes(p);
|
||
|
||
this.customEmojis = []; // [{name, url}], loaded from server
|
||
// Bundled image emoji shipped with Haven (rendered like custom emoji but
|
||
// built into the app, so every client on this version resolves them).
|
||
// Country flags are image-based on purpose: Windows browsers refuse to
|
||
// render Unicode regional-indicator flags and fall back to the bare
|
||
// two-letter code ("US", "GB", ...), so SVGs keep them consistent
|
||
// everywhere. Flag artwork: flag-icons (MIT). See emoji/flags/ATTRIBUTION.txt.
|
||
this.builtinEmojis = [
|
||
{ name: 'flag_us', url: '/emoji/flags/us.svg', keywords: 'united states america usa flag' },
|
||
{ name: 'flag_gb', url: '/emoji/flags/gb.svg', keywords: 'united kingdom britain uk british england flag' },
|
||
{ name: 'flag_ca', url: '/emoji/flags/ca.svg', keywords: 'canada canadian flag' },
|
||
{ name: 'flag_au', url: '/emoji/flags/au.svg', keywords: 'australia australian flag' },
|
||
{ name: 'flag_nz', url: '/emoji/flags/nz.svg', keywords: 'new zealand flag' },
|
||
{ name: 'flag_ie', url: '/emoji/flags/ie.svg', keywords: 'ireland irish flag' },
|
||
{ name: 'flag_fr', url: '/emoji/flags/fr.svg', keywords: 'france french flag' },
|
||
{ name: 'flag_de', url: '/emoji/flags/de.svg', keywords: 'germany german flag' },
|
||
{ name: 'flag_it', url: '/emoji/flags/it.svg', keywords: 'italy italian flag' },
|
||
{ name: 'flag_es', url: '/emoji/flags/es.svg', keywords: 'spain spanish flag' },
|
||
{ name: 'flag_pt', url: '/emoji/flags/pt.svg', keywords: 'portugal portuguese flag' },
|
||
{ name: 'flag_nl', url: '/emoji/flags/nl.svg', keywords: 'netherlands dutch holland flag' },
|
||
{ name: 'flag_be', url: '/emoji/flags/be.svg', keywords: 'belgium belgian flag' },
|
||
{ name: 'flag_lu', url: '/emoji/flags/lu.svg', keywords: 'luxembourg flag' },
|
||
{ name: 'flag_ch', url: '/emoji/flags/ch.svg', keywords: 'switzerland swiss flag' },
|
||
{ name: 'flag_at', url: '/emoji/flags/at.svg', keywords: 'austria austrian flag' },
|
||
{ name: 'flag_dk', url: '/emoji/flags/dk.svg', keywords: 'denmark danish flag' },
|
||
{ name: 'flag_no', url: '/emoji/flags/no.svg', keywords: 'norway norwegian flag' },
|
||
{ name: 'flag_se', url: '/emoji/flags/se.svg', keywords: 'sweden swedish flag' },
|
||
{ name: 'flag_fi', url: '/emoji/flags/fi.svg', keywords: 'finland finnish flag' },
|
||
{ name: 'flag_is', url: '/emoji/flags/is.svg', keywords: 'iceland flag' },
|
||
{ name: 'flag_pl', url: '/emoji/flags/pl.svg', keywords: 'poland polish flag' },
|
||
{ name: 'flag_ee', url: '/emoji/flags/ee.svg', keywords: 'estonia flag' },
|
||
{ name: 'flag_lv', url: '/emoji/flags/lv.svg', keywords: 'latvia flag' },
|
||
{ name: 'flag_lt', url: '/emoji/flags/lt.svg', keywords: 'lithuania flag' },
|
||
{ name: 'flag_cz', url: '/emoji/flags/cz.svg', keywords: 'czech czechia republic flag' },
|
||
{ name: 'flag_sk', url: '/emoji/flags/sk.svg', keywords: 'slovakia flag' },
|
||
{ name: 'flag_hu', url: '/emoji/flags/hu.svg', keywords: 'hungary flag' },
|
||
{ name: 'flag_ro', url: '/emoji/flags/ro.svg', keywords: 'romania flag' },
|
||
{ name: 'flag_bg', url: '/emoji/flags/bg.svg', keywords: 'bulgaria flag' },
|
||
{ name: 'flag_si', url: '/emoji/flags/si.svg', keywords: 'slovenia flag' },
|
||
{ name: 'flag_hr', url: '/emoji/flags/hr.svg', keywords: 'croatia flag' },
|
||
{ name: 'flag_gr', url: '/emoji/flags/gr.svg', keywords: 'greece greek flag' },
|
||
{ name: 'flag_al', url: '/emoji/flags/al.svg', keywords: 'albania flag' },
|
||
{ name: 'flag_me', url: '/emoji/flags/me.svg', keywords: 'montenegro flag' },
|
||
{ name: 'flag_mk', url: '/emoji/flags/mk.svg', keywords: 'north macedonia flag' },
|
||
{ name: 'flag_ua', url: '/emoji/flags/ua.svg', keywords: 'ukraine ukrainian flag' },
|
||
{ name: 'flag_tr', url: '/emoji/flags/tr.svg', keywords: 'turkey turkish flag' },
|
||
{ name: 'flag_jp', url: '/emoji/flags/jp.svg', keywords: 'japan japanese flag' },
|
||
{ name: 'flag_kr', url: '/emoji/flags/kr.svg', keywords: 'south korea korean flag' },
|
||
{ name: 'flag_tw', url: '/emoji/flags/tw.svg', keywords: 'taiwan flag' },
|
||
{ name: 'flag_ph', url: '/emoji/flags/ph.svg', keywords: 'philippines filipino flag' },
|
||
{ name: 'flag_th', url: '/emoji/flags/th.svg', keywords: 'thailand thai flag' },
|
||
{ name: 'flag_sg', url: '/emoji/flags/sg.svg', keywords: 'singapore flag' },
|
||
{ name: 'flag_in', url: '/emoji/flags/in.svg', keywords: 'india indian flag' },
|
||
{ name: 'flag_id', url: '/emoji/flags/id.svg', keywords: 'indonesia flag' },
|
||
{ name: 'flag_my', url: '/emoji/flags/my.svg', keywords: 'malaysia flag' },
|
||
{ name: 'flag_vn', url: '/emoji/flags/vn.svg', keywords: 'vietnam flag' },
|
||
{ name: 'flag_mn', url: '/emoji/flags/mn.svg', keywords: 'mongolia flag' },
|
||
{ name: 'flag_bd', url: '/emoji/flags/bd.svg', keywords: 'bangladesh flag' },
|
||
{ name: 'flag_lk', url: '/emoji/flags/lk.svg', keywords: 'sri lanka flag' },
|
||
{ name: 'flag_np', url: '/emoji/flags/np.svg', keywords: 'nepal flag' },
|
||
{ name: 'flag_il', url: '/emoji/flags/il.svg', keywords: 'israel israeli flag' },
|
||
{ name: 'flag_sa', url: '/emoji/flags/sa.svg', keywords: 'saudi arabia flag' },
|
||
{ name: 'flag_ae', url: '/emoji/flags/ae.svg', keywords: 'united arab emirates uae flag' },
|
||
{ name: 'flag_qa', url: '/emoji/flags/qa.svg', keywords: 'qatar flag' },
|
||
{ name: 'flag_bh', url: '/emoji/flags/bh.svg', keywords: 'bahrain flag' },
|
||
{ name: 'flag_kw', url: '/emoji/flags/kw.svg', keywords: 'kuwait flag' },
|
||
{ name: 'flag_om', url: '/emoji/flags/om.svg', keywords: 'oman flag' },
|
||
{ name: 'flag_jo', url: '/emoji/flags/jo.svg', keywords: 'jordan flag' },
|
||
{ name: 'flag_eg', url: '/emoji/flags/eg.svg', keywords: 'egypt egyptian flag' },
|
||
{ name: 'flag_ma', url: '/emoji/flags/ma.svg', keywords: 'morocco flag' },
|
||
{ name: 'flag_tn', url: '/emoji/flags/tn.svg', keywords: 'tunisia flag' },
|
||
{ name: 'flag_mx', url: '/emoji/flags/mx.svg', keywords: 'mexico mexican flag' },
|
||
{ name: 'flag_br', url: '/emoji/flags/br.svg', keywords: 'brazil brazilian flag' },
|
||
{ name: 'flag_ar', url: '/emoji/flags/ar.svg', keywords: 'argentina flag' },
|
||
{ name: 'flag_cl', url: '/emoji/flags/cl.svg', keywords: 'chile flag' },
|
||
{ name: 'flag_co', url: '/emoji/flags/co.svg', keywords: 'colombia flag' },
|
||
{ name: 'flag_pe', url: '/emoji/flags/pe.svg', keywords: 'peru flag' },
|
||
{ name: 'flag_uy', url: '/emoji/flags/uy.svg', keywords: 'uruguay flag' },
|
||
{ name: 'flag_ec', url: '/emoji/flags/ec.svg', keywords: 'ecuador flag' },
|
||
{ name: 'flag_cr', url: '/emoji/flags/cr.svg', keywords: 'costa rica flag' },
|
||
{ name: 'flag_pa', url: '/emoji/flags/pa.svg', keywords: 'panama flag' },
|
||
{ name: 'flag_gt', url: '/emoji/flags/gt.svg', keywords: 'guatemala flag' },
|
||
{ name: 'flag_do', url: '/emoji/flags/do.svg', keywords: 'dominican republic flag' },
|
||
{ name: 'flag_jm', url: '/emoji/flags/jm.svg', keywords: 'jamaica flag' },
|
||
{ name: 'flag_bs', url: '/emoji/flags/bs.svg', keywords: 'bahamas flag' },
|
||
{ name: 'flag_tt', url: '/emoji/flags/tt.svg', keywords: 'trinidad tobago flag' },
|
||
{ name: 'flag_za', url: '/emoji/flags/za.svg', keywords: 'south africa flag' },
|
||
{ name: 'flag_ke', url: '/emoji/flags/ke.svg', keywords: 'kenya flag' },
|
||
{ name: 'flag_ng', url: '/emoji/flags/ng.svg', keywords: 'nigeria flag' },
|
||
{ name: 'flag_gh', url: '/emoji/flags/gh.svg', keywords: 'ghana flag' },
|
||
{ name: 'flag_sn', url: '/emoji/flags/sn.svg', keywords: 'senegal flag' },
|
||
{ name: 'flag_rw', url: '/emoji/flags/rw.svg', keywords: 'rwanda flag' },
|
||
{ name: 'flag_bw', url: '/emoji/flags/bw.svg', keywords: 'botswana flag' },
|
||
{ name: 'flag_ci', url: '/emoji/flags/ci.svg', keywords: 'ivory coast cote divoire flag' },
|
||
{ name: 'flag_tz', url: '/emoji/flags/tz.svg', keywords: 'tanzania flag' },
|
||
{ name: 'us_betsy_ross', url: '/emoji/us-betsy-ross.svg', keywords: 'betsy ross flag united states america usa stars stripes colonies historical patriotic' },
|
||
{ name: 'gadsden', url: '/emoji/us-gadsden.svg', keywords: 'gadsden dont tread on me snake rattlesnake flag united states america usa liberty patriotic' },
|
||
];
|
||
this.stickers = []; // [{id, name, pack_name, url}], loaded from server
|
||
this._emojiPickerContext = 'main'; // 'main' | 'thread' | 'dmpip', set by emoji button handlers
|
||
this._emojiPickerSection = 'emoji'; // 'emoji' | 'sticker': last-used picker tab
|
||
|
||
this._init();
|
||
}
|
||
|
||
// ── Initialization ────────────────────────────────────
|
||
|
||
_init() {
|
||
// Kick this off before anything can render a message. Until it resolves,
|
||
// _imgSrcAttr parks remote images in data-mp-src rather than emitting a
|
||
// raw external src, so there is no window in which an image leaks. (v3.43.0)
|
||
this._mediaProxyEnabled = true; // assume on: fail closed, not open
|
||
this._mediaToken = null;
|
||
this._loadMediaToken?.();
|
||
this._startMediaTokenRefresh?.(); // the token expires after ~2 days
|
||
this._setupMediaTokenRetry?.(); // and a stale one used to fail silently
|
||
// Link policy for decrypted DM content (#5483). Requested once the socket
|
||
// exists, below, since it travels over the authenticated connection.
|
||
|
||
this.socket = io({
|
||
// presenceDeltas: this client merges member-list changes
|
||
// (online-users-delta) instead of needing every list in full.
|
||
auth: { token: this.token, presenceDeltas: 1 },
|
||
reconnectionDelay: 1500,
|
||
reconnectionDelayMax: 10000,
|
||
randomizationFactor: 0.4,
|
||
});
|
||
// Expose socket globally so plugin-loader can emit set-preference
|
||
// when the user activates a published file theme (#5359)
|
||
window.havenSocket = this.socket;
|
||
this.voice = new VoiceManager(this.socket);
|
||
if (this.user && this.user.id) this.voice.localUserId = this.user.id;
|
||
|
||
// CRITICAL FIX: Run avatar setup first and use delegation to ensure listeners work
|
||
this._setupAvatarUpload();
|
||
|
||
this._setupSocketListeners();
|
||
this._initLinkPolicy?.();
|
||
this._setupUI();
|
||
this._setupThemes();
|
||
this._setupThemePicker();
|
||
this._setupServerBar();
|
||
this._applyGuestMode(); // (#5381) hide DM UI / lock down features for guests
|
||
this._setupNotifications();
|
||
this._setupMasterVolume();
|
||
this._setupPushNotifications();
|
||
this._resyncDesktopBadgeOnFocus?.();
|
||
this._setupImageUpload();
|
||
this._setupGifPicker();
|
||
this._startStatusBar();
|
||
this._setupMobile();
|
||
this._setupMobileSidebarServers();
|
||
this._setupCollapsibleSections();
|
||
this._setupIOSKeyboard();
|
||
this._setupMobileBridge();
|
||
this._setupStatusPicker();
|
||
this._setupFileUpload();
|
||
this._setupIdleDetection();
|
||
this._setupSoundManagement();
|
||
this._setupEmojiManagement();
|
||
this._setupStickerManagement();
|
||
this._setupWebhookManagement();
|
||
this._setupExtensionUpdates();
|
||
this._setupDiscordImport();
|
||
this._setupServerTemplates();
|
||
this._setupAuditLog();
|
||
this._initRoleManagement();
|
||
this._initPermMatrix();
|
||
this._initServerBranding();
|
||
this._setupResizableSidebars();
|
||
this.modMode = typeof ModMode === 'function' ? new ModMode() : null;
|
||
this.modMode?.init();
|
||
this._setupDensityPicker();
|
||
this._setupChannelScrollPicker();
|
||
this._setupToggleStylePicker();
|
||
this._setupAnimatePfpPicker();
|
||
this._setupAnimateChatPicker();
|
||
this._setupZoomSlider();
|
||
this._setupAccessibility();
|
||
this._setupEmojiSizePicker();
|
||
this._setupImageModePicker();
|
||
this._setupLazyMedia();
|
||
this._setupFeedAnchor();
|
||
this._setupEmbedSizePicker();
|
||
this._setupRoleDisplayPicker();
|
||
this._setupToolbarIconPicker();
|
||
// After the pickers above: the defaults apply through them (#5739).
|
||
this._setupMemberDefaults();
|
||
this._setupMessageHighlights();
|
||
this._setupModCases();
|
||
this._setupBadges();
|
||
this._setupServerMute();
|
||
this._setupForumUnread();
|
||
this._setupThreadUnread();
|
||
this._setupThreadScroll();
|
||
this._setupThreadDock();
|
||
this._setupTyping();
|
||
this._setupMessageNsfw();
|
||
this._setupMediaPlayback();
|
||
this._setupSilentToggle();
|
||
this._setupCategoryLayoutPicker();
|
||
this._setupDebugSection();
|
||
this._setupLightbox();
|
||
this._setupImageZoom();
|
||
this._setupMediaGrid();
|
||
this._setupOnlineOverlay();
|
||
this._setupModalExpand();
|
||
this._checkForUpdates();
|
||
this._initDesktopAppBanner();
|
||
this._initAndroidBetaBanner();
|
||
this._initWelcomePopups();
|
||
this._initMoveMessages();
|
||
this._initMoveToThread();
|
||
|
||
// CSP-safe image error handling (no inline onerror attributes)
|
||
// For avatar images, hide the broken img and show the letter-initial fallback
|
||
const avatarErrorHandler = (e) => {
|
||
if (e.target.tagName === 'IMG') {
|
||
e.target.style.display = 'none';
|
||
const fallback = e.target.nextElementSibling;
|
||
if (fallback && (fallback.classList.contains('message-avatar') || fallback.classList.contains('user-item-avatar'))) {
|
||
fallback.style.display = 'flex';
|
||
}
|
||
}
|
||
};
|
||
document.getElementById('messages')?.addEventListener('error', avatarErrorHandler, true);
|
||
document.getElementById('online-users')?.addEventListener('error', avatarErrorHandler, true);
|
||
|
||
this.socket.emit('get-channels');
|
||
this.socket.emit('get-server-settings');
|
||
this.socket.emit('get-preferences');
|
||
this.socket.emit('get-high-scores', { game: 'flappy' });
|
||
|
||
// ── Auto-start performance diagnostics after startup settles ──
|
||
setTimeout(() => this._startPerfDiagnostics(), 30000);
|
||
|
||
// E2E init is deferred to 'session-info' handler to ensure
|
||
// the socket is fully connected and server-side handlers are registered.
|
||
|
||
document.getElementById('current-user').textContent = this.user.displayName || this.user.username;
|
||
const loginEl = document.getElementById('login-name');
|
||
if (loginEl) loginEl.textContent = `@${this.user.username}`;
|
||
|
||
if (this.user.isAdmin || this._hasGlobalPerm('create_channel')) {
|
||
document.getElementById('admin-controls').style.display = 'block';
|
||
}
|
||
if (this.user.isAdmin || this._hasPerm('manage_roles') || this._hasPerm('manage_server')) {
|
||
document.getElementById('admin-mod-panel').style.display = 'block';
|
||
}
|
||
if (this.user.isAdmin || this._hasGlobalPerm('invite_users') || this._hasPerm('manage_server')) {
|
||
document.getElementById('sidebar-invite-panel').style.display = 'block';
|
||
}
|
||
const organizeBtn = document.getElementById('organize-channels-btn');
|
||
if (organizeBtn) organizeBtn.style.display = '';
|
||
|
||
document.getElementById('mod-mode-settings-toggle')?.addEventListener('click', () => this.modMode?.toggle());
|
||
}
|
||
|
||
async _loadBotCommands() {
|
||
try {
|
||
const res = await fetch('/api/bot-commands', { headers: { 'Authorization': `Bearer ${this.token}` } });
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
if (!data.commands || !data.commands.length) return;
|
||
const known = new Map(this.slashCommands.map(c => [String(c.cmd || '').toLowerCase(), c]));
|
||
for (const bc of data.commands) {
|
||
const cmd = String(bc.command || '').trim();
|
||
if (!cmd) continue;
|
||
const key = cmd.toLowerCase();
|
||
const channelCode = bc.channel_code || null;
|
||
const desc = `${bc.description || t('commands.bot_command')} [${bc.bot_name || t('commands.bot')}]`;
|
||
const existing = known.get(key);
|
||
if (existing) {
|
||
// The same command registered by a second bot in another channel
|
||
// keeps the one menu entry and adds its channel to it, so the
|
||
// suggestions show in every channel that has a bot for it, each
|
||
// naming its own bot. A built-in command of the same name stays as
|
||
// it is (#5635).
|
||
if (channelCode && Array.isArray(existing.channelCodes) && !existing.channelCodes.includes(channelCode)) {
|
||
existing.channelCodes.push(channelCode);
|
||
existing.descByChannel[channelCode] = desc;
|
||
}
|
||
continue;
|
||
}
|
||
const entry = {
|
||
cmd,
|
||
// Bot commands can have arbitrary args; a hardcoded "<...>" makes
|
||
// subcommand entries look broken and encourages base-command clicks.
|
||
args: '',
|
||
desc,
|
||
// A bot lives in one channel, so its commands are only offered there (#5635).
|
||
channelCodes: channelCode ? [channelCode] : null,
|
||
descByChannel: channelCode ? { [channelCode]: desc } : {}
|
||
};
|
||
known.set(key, entry);
|
||
this.slashCommands.push(entry);
|
||
}
|
||
} catch (err) { console.warn('[Commands] could not load bot slash commands', err); }
|
||
}
|
||
|
||
}
|
||
|
||
// ── Merge all method groups onto the prototype ────────────
|
||
Object.assign(HavenApp.prototype,
|
||
SocketMethods,
|
||
SocketChannelMethods,
|
||
SocketEventMethods,
|
||
UIBindMethods,
|
||
ComposerMethods,
|
||
ComposerListMethods,
|
||
ChannelMenuMethods,
|
||
VoiceControlMethods,
|
||
MediaGalleryMethods,
|
||
MessageActionMethods,
|
||
PipPanelMethods,
|
||
PeopleMethods,
|
||
SettingsMethods,
|
||
AdminControlMethods,
|
||
ExtensionUpdateMethods,
|
||
ServerBarMethods,
|
||
MobileMethods,
|
||
MediaMethods,
|
||
ImageZoomMethods,
|
||
MediaGridMethods,
|
||
FeedAnchorMethods,
|
||
AttachmentMethods,
|
||
AvatarEditorMethods,
|
||
AppearanceMethods,
|
||
AccessibilityMethods,
|
||
SoundMethods,
|
||
EmojiStickerMethods,
|
||
BotMethods,
|
||
ContextMethods,
|
||
ChannelMethods,
|
||
ChannelContextMethods,
|
||
ChannelOrganizeMethods,
|
||
ChannelUnreadMethods,
|
||
MessageMethods,
|
||
LinkPreviewMethods,
|
||
MessageToolMethods,
|
||
UserMethods,
|
||
BirthdayMethods,
|
||
VoiceMethods,
|
||
ScreenWebcamMethods,
|
||
StreamTileMethods,
|
||
MusicMethods,
|
||
MusicKeyMethods,
|
||
UtilityMethods,
|
||
MessageContentMethods,
|
||
EmojiPickerMethods,
|
||
GifPickerMethods,
|
||
ThreadMethods,
|
||
AdminMethods,
|
||
BrandingMethods,
|
||
MembersAdminMethods,
|
||
AutocompleteMethods,
|
||
StatusMethods,
|
||
DiscordImportMethods,
|
||
RoleMethods,
|
||
RoleAssignMethods,
|
||
ModerationMethods,
|
||
PlatformMethods,
|
||
SearchMethods,
|
||
FerryMethods,
|
||
ForumMethods,
|
||
ForumVoteMethods,
|
||
ForumBlogMethods,
|
||
ForumPictureVoteMethods,
|
||
RoleToolMethods,
|
||
PermMatrixMethods,
|
||
CallMethods,
|
||
ScalingMethods,
|
||
GroupMethods,
|
||
TemplateMethods,
|
||
MemberDefaultsMethods,
|
||
MessageHighlightMethods,
|
||
MessageGroupingMethods,
|
||
ModCaseMethods,
|
||
ServerMuteMethods,
|
||
AdminReauthMethods,
|
||
MasterVolumeMethods,
|
||
ForumUnreadMethods,
|
||
ThreadUnreadMethods,
|
||
ThreadDockMethods,
|
||
TypingMethods,
|
||
MessageNsfwMethods,
|
||
MediaPlaybackMethods,
|
||
PermDraftMethods,
|
||
ThreadScrollMethods,
|
||
MoveToThreadMethods,
|
||
ChannelCategoryMethods,
|
||
ChannelPersonalOrderMethods,
|
||
PushToMuteMethods,
|
||
BadgeMethods,
|
||
ThemePickerMethods,
|
||
SilentMethods,
|
||
);
|
||
|
||
// ── Boot ───────────────────────────────────────────────
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
await window.i18n?.init();
|
||
window.app = new HavenApp();
|
||
});
|