diff --git a/src/client/webapp/elements/context-menu-conn.tsx b/src/client/webapp/elements/context-menu-conn.tsx deleted file mode 100644 index 7231b44..0000000 --- a/src/client/webapp/elements/context-menu-conn.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import ElementsUtil from './require/elements-util.js'; -import BaseElements from './require/base-elements.js'; - -import Q from '../q-module.js'; -import UI from '../ui.js'; -import CombinedGuild from '../guild-combined.js'; -import PersonalizeOverlay from './overlays/overlay-personalize.js'; - -export default function createConnectionContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild) { - const statuses = [ 'online', 'away', 'busy', 'invisible' ]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let content: JSX.Element[] = [ -
-
-
Personalize
-
, -
- ]; - content = content.concat(statuses.map(status => ( -
-
-
{status}
-
- ))); - const element = BaseElements.createContextMenu(document,
{content}
); - - q.$$$(element, '.personalize').addEventListener('click', async () => { - element.removeSelf(); - if (ui.activeConnection === null) return; - ElementsUtil.presentReactOverlay(document, ); - }); - - for (const status of statuses) { - q.$$$(element, '.' + status).addEventListener('click', async () => { - element.removeSelf(); - const currentConnection = await guild.fetchConnectionInfo(); - if (status != currentConnection.status) { - await guild.requestSetStatus(status); - } - }); - } - - return element; -} diff --git a/src/client/webapp/elements/context-menus/components/context-menu.tsx b/src/client/webapp/elements/context-menus/components/context-menu.tsx new file mode 100644 index 0000000..78d8007 --- /dev/null +++ b/src/client/webapp/elements/context-menus/components/context-menu.tsx @@ -0,0 +1,38 @@ +import React, { FC, ReactNode, RefObject, useEffect, useMemo, useRef, useState } from 'react'; +import ElementsUtil, { IAlignment } from '../../require/elements-util'; +import ReactHelper from '../../require/react-helper'; + +export interface ContextMenuProps { + relativeToRef: RefObject; + alignment: IAlignment; + children: ReactNode; + close: () => void; +} + +const ContextMenu: FC = (props: ContextMenuProps) => { + const { relativeToRef, alignment, children, close } = props; + + const rootRef = useRef(null); + + const [ aligned, setAligned ] = useState(false); + + ReactHelper.useCloseWhenClickedOutsideEffect(rootRef, close); + + useEffect(() => { + if (!rootRef.current || !relativeToRef.current) return; + ElementsUtil.alignContextElement(rootRef.current, relativeToRef.current, alignment); + setAligned(true); + }, [ rootRef, relativeToRef ]); + + const contextClass = useMemo(() => { + return 'context react' + (aligned ? ' aligned' : ''); + }, [ aligned ]); + + return ( +
+
{children}
+
+ ); +} + +export default ContextMenu; diff --git a/src/client/webapp/elements/context-menus/context-connection-info.tsx b/src/client/webapp/elements/context-menus/context-connection-info.tsx new file mode 100644 index 0000000..63c9477 --- /dev/null +++ b/src/client/webapp/elements/context-menus/context-connection-info.tsx @@ -0,0 +1,59 @@ +import React, { FC, RefObject, useCallback, useMemo } from 'react'; +import { Member } from '../../data-types'; +import CombinedGuild from '../../guild-combined'; +import PersonalizeOverlay from '../overlays/overlay-personalize'; +import ElementsUtil from '../require/elements-util'; +import ContextMenu from './components/context-menu'; + +export interface ConnectionInfoContextProps { + guild: CombinedGuild; + selfMember: Member; + relativeToRef: RefObject; + close: () => void; +} + +const ConnectionInfoContext: FC = (props: ConnectionInfoContextProps) => { + const { guild, selfMember, relativeToRef, close } = props; + + const setSelfStatus = useCallback(async (status: string) => { + await guild.requestSetStatus(status); + }, [ guild ]); + + const statusElements = useMemo(() => { + return [ 'online', 'away', 'busy', 'invisible' ].map(status => { + // Note: throwing out setSelfStatus promise + return ( +
{ setSelfStatus(status); close(); }}> +
+
{status}
+
+ ); + }); + }, [ setSelfStatus ]); + + const openPersonalize = useCallback(() => { + close(); + // Note: using global document, not very safe >:| + // TODO: Do this in full react (also fixes global document problem) + ElementsUtil.presentReactOverlay(document, ); + }, [ guild, selfMember, close ]); + + const alignment = useMemo(() => { + return { bottom: 'top', centerX: 'centerX' } + }, []); + + return ( + +
+
+
+
Personalize
+
+
+ {statusElements} +
+ + ); +} + +export default ConnectionInfoContext; diff --git a/src/client/webapp/elements/displays/display-guild-invites.tsx b/src/client/webapp/elements/displays/display-guild-invites.tsx index c386a9f..357a8f0 100644 --- a/src/client/webapp/elements/displays/display-guild-invites.tsx +++ b/src/client/webapp/elements/displays/display-guild-invites.tsx @@ -14,7 +14,6 @@ import moment from 'moment'; import DropdownInput from '../components/input-dropdown'; import Button, { ButtonColorType } from '../components/button'; import GuildSubscriptions from '../require/guild-subscriptions'; -import ElementsUtil from '../require/elements-util'; import BaseElements from '../require/base-elements'; @@ -33,11 +32,7 @@ const GuildInvitesDisplay: FC = (props: GuildInvitesDi const [ expiresFromNow, setExpiresFromNow ] = useState(moment.duration(1, 'day')); const [ expiresFromNowText, setExpiresFromNowText ] = useState('1 day'); - const [ iconSrc ] = ReactHelper.useOneTimeAsyncAction( - async () => await ElementsUtil.getImageSrcFromResourceFailSoftly(guild, guildMeta?.iconResourceId ?? null), - './img/loading.svg', - [ guild, guildMeta?.iconResourceId ] - ); + const [ iconSrc ] = GuildSubscriptions.useSoftImageSrcResourceSubscription(guild, guildMeta?.iconResourceId ?? null); useEffect(() => { if (expiresFromNowText === 'never') { diff --git a/src/client/webapp/elements/events-connection.ts b/src/client/webapp/elements/events-connection.ts deleted file mode 100644 index 4a11511..0000000 --- a/src/client/webapp/elements/events-connection.ts +++ /dev/null @@ -1,16 +0,0 @@ -import ElementsUtil from './require/elements-util.js'; - -import Q from '../q-module.js'; -import UI from '../ui.js'; -import createConnectionContextMenu from './context-menu-conn'; - -export default function bindConnectionEvents(document: Document, q: Q, ui: UI): void { - q.$('#connection').addEventListener('click', () => { - if (ui.activeGuild === null) return; - if (!ui.activeGuild.isSocketVerified()) return; - - const contextMenu = createConnectionContextMenu(document, q, ui, ui.activeGuild); - document.body.appendChild(contextMenu); - ElementsUtil.alignContextElement(contextMenu, q.$('#connection'), { bottom: 'top', centerX: 'centerX' }); - }); -} diff --git a/src/client/webapp/elements/lists/components/member-element.tsx b/src/client/webapp/elements/lists/components/member-element.tsx index 904a957..12bb63c 100644 --- a/src/client/webapp/elements/lists/components/member-element.tsx +++ b/src/client/webapp/elements/lists/components/member-element.tsx @@ -1,22 +1,25 @@ import React, { FC, useMemo } from 'react'; import { Member } from '../../../data-types'; import CombinedGuild from '../../../guild-combined'; -import ElementsUtil from '../../require/elements-util'; -import ReactHelper from '../../require/react-helper'; +import GuildSubscriptions from '../../require/guild-subscriptions'; + +export interface DummyMember { + id: 'dummy'; + displayName: string; + status: string; + roleColor: null; + avatarResourceId: null; +} export interface MemberProps { guild: CombinedGuild; - member: Member; + member: Member | DummyMember; } const MemberElement: FC = (props: MemberProps) => { const { guild, member } = props; - const [ avatarSrc ] = ReactHelper.useOneTimeAsyncAction( - async () => ElementsUtil.getImageSrcFromResourceFailSoftly(guild, member.avatarResourceId), - './img/loading.svg', - [ guild, member.avatarResourceId ] - ); + const [ avatarSrc ] = GuildSubscriptions.useSoftImageSrcResourceSubscription(guild, member.avatarResourceId); const nameStyle = useMemo(() => member.roleColor ? { color: member.roleColor } : {}, [ member.roleColor ]); diff --git a/src/client/webapp/elements/lists/components/message-element.tsx b/src/client/webapp/elements/lists/components/message-element.tsx index e724067..d24b012 100644 --- a/src/client/webapp/elements/lists/components/message-element.tsx +++ b/src/client/webapp/elements/lists/components/message-element.tsx @@ -4,6 +4,7 @@ import { Member, Message } from '../../../data-types'; import CombinedGuild from '../../../guild-combined'; import ImageOverlay from '../../overlays/overlay-image'; import ElementsUtil from '../../require/elements-util'; +import GuildSubscriptions from '../../require/guild-subscriptions'; import ReactHelper from '../../require/react-helper'; interface ResourceElementProps { @@ -49,11 +50,7 @@ interface PreviewImageElementProps { const PreviewImageElement: FC = (props: PreviewImageElementProps) => { const { guild, previewWidth, previewHeight, resourcePreviewId, resourceId, resourceName } = props; - const [ imgSrc ] = ReactHelper.useOneTimeAsyncAction( - async () => await ElementsUtil.getImageSrcFromResourceFailSoftly(guild, resourcePreviewId), - './img/loading.svg', - [ guild, resourcePreviewId ] - ); + const [ imgSrc ] = GuildSubscriptions.useSoftImageSrcResourceSubscription(guild, resourcePreviewId); const clickCallback = useCallback(() => { // Note: document here isn't 100% guaranteed but we should be getting rid of this eventually anyway diff --git a/src/client/webapp/elements/mounts.tsx b/src/client/webapp/elements/mounts.tsx index 6ff3474..b294002 100644 --- a/src/client/webapp/elements/mounts.tsx +++ b/src/client/webapp/elements/mounts.tsx @@ -6,6 +6,7 @@ import ElementsUtil from "./require/elements-util"; import MemberList from "./lists/member-list"; import MessageList from './lists/message-list'; import ChannelTitle from './sections/channel-title'; +import ConnectionInfo from './sections/connection-info'; export function mountBaseComponents() { // guild-list @@ -13,6 +14,10 @@ export function mountBaseComponents() { } export function mountGuildComponents(q: Q, guild: CombinedGuild) { + // connection info + ElementsUtil.unmountReactComponent(q.$('.connection-anchor')); + ElementsUtil.mountReactComponent(q.$('.connection-anchor'), ); + // member-list ElementsUtil.unmountReactComponent(q.$('.member-list-anchor')); ElementsUtil.mountReactComponent(q.$('.member-list-anchor'), ); diff --git a/src/client/webapp/elements/overlays/overlay-image.tsx b/src/client/webapp/elements/overlays/overlay-image.tsx index f19faaa..0be64e1 100644 --- a/src/client/webapp/elements/overlays/overlay-image.tsx +++ b/src/client/webapp/elements/overlays/overlay-image.tsx @@ -26,6 +26,7 @@ const ImageOverlay: FC = (props: ImageOverlayProps) => { // TODO: Handle errors const [ resource, resourceError ] = GuildSubscriptions.useResourceSubscription(guild, resourceId); + // Note: We want this customization here since we need the resource buffer. const [ resourceImgSrc, resourceImgSrcError ] = ReactHelper.useOneTimeAsyncAction( async () => await ElementsUtil.getImageSrcFromBufferFailSoftly(resource?.data ?? null), './img/loading.svg', diff --git a/src/client/webapp/elements/overlays/overlay-personalize.tsx b/src/client/webapp/elements/overlays/overlay-personalize.tsx index 2863e32..83116b0 100644 --- a/src/client/webapp/elements/overlays/overlay-personalize.tsx +++ b/src/client/webapp/elements/overlays/overlay-personalize.tsx @@ -4,7 +4,7 @@ import Logger from '../../../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); import React, { createRef, FC, MutableRefObject, useEffect, useMemo, useState } from 'react'; -import { ConnectionInfo } from '../../data-types'; +import { ConnectionInfo, Member } from '../../data-types'; import Globals from '../../globals'; import CombinedGuild from '../../guild-combined'; import ImageEditInput from '../components/input-image-edit'; @@ -18,23 +18,23 @@ import Button from '../components/button'; export interface PersonalizeOverlayProps { document: Document; guild: CombinedGuild; - connection: ConnectionInfo; + selfMember: ConnectionInfo | Member; } const PersonalizeOverlay: FC = (props: PersonalizeOverlayProps) => { - const { document, guild, connection } = props; + const { document, guild, selfMember } = props; - if (connection.avatarResourceId === null) { + if (selfMember.avatarResourceId === null) { throw new Error('bad avatar'); } - const [ avatarResource, avatarResourceError ] = GuildSubscriptions.useResourceSubscription(guild, connection.avatarResourceId) + const [ avatarResource, avatarResourceError ] = GuildSubscriptions.useResourceSubscription(guild, selfMember.avatarResourceId) const displayNameInputRef = createRef(); - const [ savedDisplayName, setSavedDisplayName ] = useState(connection.displayName); + const [ savedDisplayName, setSavedDisplayName ] = useState(selfMember.displayName); const [ savedAvatarBuff, setSavedAvatarBuff ] = useState(null); - const [ displayName, setDisplayName ] = useState(connection.displayName); + const [ displayName, setDisplayName ] = useState(selfMember.displayName); const [ avatarBuff, setAvatarBuff ] = useState(null); const [ displayNameInputValid, setDisplayNameInputValid ] = useState(false); diff --git a/src/client/webapp/elements/require/base-elements.tsx b/src/client/webapp/elements/require/base-elements.tsx index 82adad9..ed540ae 100644 --- a/src/client/webapp/elements/require/base-elements.tsx +++ b/src/client/webapp/elements/require/base-elements.tsx @@ -14,7 +14,7 @@ import { Channel } from '../../data-types'; import CombinedGuild from '../../guild-combined'; import Q from '../../q-module'; import ReactHelper from './react-helper'; - + export interface HTMLElementWithRemoveSelf extends HTMLElement { removeSelf: (() => void); } @@ -240,9 +240,7 @@ export default class BaseElements { Z` } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any static createContextMenu(document: Document, content: JSX.Element): HTMLElementWithRemoveSelf { - // eslint-disable-next-line @typescript-eslint/no-explicit-any const element = ReactHelper.createElementFromJSX(
{content}
@@ -252,15 +250,12 @@ export default class BaseElements { element.addEventListener('mousedown', (e: Event) => { e.stopPropagation(); // stop the bubble }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any element.removeSelf = () => { if (element.parentElement) { element.parentElement.removeChild(element); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any document.body.removeEventListener('mousedown', element.removeSelf); }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any document.body.addEventListener('mousedown', element.removeSelf); return element as HTMLElementWithRemoveSelf; } diff --git a/src/client/webapp/elements/require/elements-util.tsx b/src/client/webapp/elements/require/elements-util.tsx index 55388f0..dae449c 100644 --- a/src/client/webapp/elements/require/elements-util.tsx +++ b/src/client/webapp/elements/require/elements-util.tsx @@ -20,7 +20,7 @@ import React from 'react'; import Overlay from '../components/overlay'; import ReactDOM from 'react-dom'; -interface IAlignment { +export interface IAlignment { left?: string; centerX?: string; right?: string; @@ -96,6 +96,7 @@ export default class ElementsUtil { element.classList.remove('shaking-horizontal'); } + // TODO: Remove this in favor of useSubmitButton style stuff from ReactHelper // Calls a function with the start parameter and then the inverse of the start parameter after a determined number of ms // There is no way to cancel this function // Useful for enabling a "shake" element for a pre-determined amount of time @@ -105,20 +106,6 @@ export default class ElementsUtil { setState(old => !start); } - static createShakingOnSubmit(props: ShakingOnSubmitProps): () => Promise { - const { doSubmit, setSubmitting, setSubmitFailed, setShaking } = props; - - return async () => { - setSubmitting(true); - const succeeded = await doSubmit(); - setSubmitting(false); - setSubmitFailed(!succeeded); - if (!succeeded) { - await ElementsUtil.delayToggleState(setShaking, 400); - } - } - } - static async getImageBufferSrc(buffer: Buffer): Promise { const result = await FileType.fromBuffer(buffer); switch (result && result.mime) { @@ -148,6 +135,7 @@ export default class ElementsUtil { } } + // Avoid this function. Use GuildSubscriptions.useSoftImgSrcResourceSubscription instead static async getImageSrcFromResourceFailSoftly(guild: CombinedGuild, resourceId: string | null): Promise { if (resourceId === null) { return './img/loading.svg'; diff --git a/src/client/webapp/elements/require/guild-subscriptions.ts b/src/client/webapp/elements/require/guild-subscriptions.ts index 89abb9f..7489b04 100644 --- a/src/client/webapp/elements/require/guild-subscriptions.ts +++ b/src/client/webapp/elements/require/guild-subscriptions.ts @@ -14,6 +14,7 @@ import { IDQuery, PartialMessageListQuery } from '../../auto-verifier-with-args' import { Token, Channel } from '../../data-types'; import ReactHelper from './react-helper'; import Globals from '../../globals'; +import ElementsUtil from './elements-util'; export type SingleSubscriptionEvents = { 'fetch': () => void; @@ -573,6 +574,22 @@ export default class GuildSubscriptions { }, fetchResourceFunc); } + static useSoftImageSrcResourceSubscription(guild: CombinedGuild, resourceId: string | null): [ imgSrc: string ] { + const [ value, fetchError ] = GuildSubscriptions.useResourceSubscription(guild, resourceId); + + const [ imgSrc ] = ReactHelper.useOneTimeAsyncAction( + async () => { + if (fetchError) return './img/error.png'; + if (!value) return './img/loading.svg'; + return await ElementsUtil.getImageSrcFromBufferFailSoftly(value.data); + }, + './img/loading.svg', + [ value, fetchError ] + ); + + return [ imgSrc ]; + } + static useChannelsSubscription(guild: CombinedGuild) { const fetchChannelsFunc = useCallback(async () => { return await guild.fetchChannels(); @@ -607,6 +624,26 @@ export default class GuildSubscriptions { }, fetchMembersFunc); } + static useSelfMemberSubscription(guild: CombinedGuild): [ selfMember: Member | null ] { + const [ fetchRetryCallable, members, fetchError ] = GuildSubscriptions.useMembersSubscription(guild); + + // TODO: Show an error if we can't fetch and allow retry + + const selfMember = useMemo(() => { + if (members) { + const member = members.find(m => m.id === guild.memberId); + if (!member) { + LOG.warn('Unable to find self in members'); + return null; + } + return member; + } + return null; + }, [ guild.memberId, members ]); + + return [ selfMember ]; + } + static useTokensSubscription(guild: CombinedGuild) { const fetchTokensFunc = useCallback(async () => { //LOG.silly('fetching tokens for subscription'); diff --git a/src/client/webapp/elements/require/react-helper.ts b/src/client/webapp/elements/require/react-helper.ts index 130a5d2..290e7f0 100644 --- a/src/client/webapp/elements/require/react-helper.ts +++ b/src/client/webapp/elements/require/react-helper.ts @@ -3,7 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console; import Logger from '../../../../logger/logger'; const LOG = Logger.create(__filename, electronConsole); -import { DependencyList, Dispatch, MutableRefObject, SetStateAction, UIEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { DependencyList, Dispatch, MutableRefObject, RefObject, SetStateAction, UIEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import ReactDOMServer from "react-dom/server"; import { ShouldNeverHappenError } from "../../data-types"; import Util from '../../util'; @@ -337,4 +337,21 @@ export default class ReactHelper { return [ onScrollCallable, onLoadCallable, loadAbove, loadBelow ]; } + + static useCloseWhenClickedOutsideEffect(ref: RefObject, close: () => void) { + const handleClickOutside = useCallback((event: MouseEvent) => { + if (!ref.current) return; + // Casting here is OK. https://stackoverflow.com/q/61164018 + if (ref.current.contains(event.target as Node)) return; + + close(); + }, [ ref, close ]); + + useEffect(() => { + document.addEventListener('click', handleClickOutside); + return () => { + document.removeEventListener('click', handleClickOutside); + }; + }, [ handleClickOutside ]); + } } diff --git a/src/client/webapp/elements/sections/connection-info.tsx b/src/client/webapp/elements/sections/connection-info.tsx new file mode 100644 index 0000000..f6c3460 --- /dev/null +++ b/src/client/webapp/elements/sections/connection-info.tsx @@ -0,0 +1,58 @@ +import * as electronRemote from '@electron/remote'; +const electronConsole = electronRemote.getGlobal('console') as Console; +import Logger from '../../../../logger/logger'; +const LOG = Logger.create(__filename, electronConsole); + +import React, { FC, useCallback, useMemo, useRef, useState } from 'react'; +import { Member } from '../../data-types'; +import CombinedGuild from '../../guild-combined'; +import ConnectionInfoContext from '../context-menus/context-connection-info'; +import MemberElement, { DummyMember } from '../lists/components/member-element'; +import GuildSubscriptions from '../require/guild-subscriptions'; + +export interface ConnectionInfoProps { + guild: CombinedGuild; +} + +const ConnectionInfo: FC = (props: ConnectionInfoProps) => { + const { guild } = props; + + const rootRef = useRef(null); + + // TODO: Respond to and emit global context menu events to prevent multiple + // context menus from being open at once. (maybe this isn't very reacty though) + const [ contextMenuOpen, setContextMenuOpen ] = useState(false); + + const [ selfMember ] = GuildSubscriptions.useSelfMemberSubscription(guild); + + const displayMember = useMemo((): Member | DummyMember => { + if (!selfMember) { + return { + id: 'dummy', + displayName: 'Connecting...', + status: 'unknown', + roleColor: null, + avatarResourceId: null + }; + } + return selfMember; + }, [ selfMember ]); + + const contextMenu = useMemo(() => { + if (!selfMember) return null; + return { console.log('close'); setContextMenuOpen(false); }} /> + }, [ guild, selfMember, rootRef ]); + + const toggleContextMenu = useCallback(() => { + setContextMenuOpen(oldContextMenuOpen => !!contextMenu && !oldContextMenuOpen); + }, [ contextMenu ]); + + return ( +
+
+ {contextMenuOpen ? contextMenu : null} +
+ ); +} + +export default ConnectionInfo; diff --git a/src/client/webapp/index.html b/src/client/webapp/index.html index 00b024a..f6d5695 100644 --- a/src/client/webapp/index.html +++ b/src/client/webapp/index.html @@ -46,16 +46,7 @@
- +
diff --git a/src/client/webapp/preload.ts b/src/client/webapp/preload.ts index 6c5b920..53cdfe6 100644 --- a/src/client/webapp/preload.ts +++ b/src/client/webapp/preload.ts @@ -18,7 +18,6 @@ import { Changes, Channel, ConnectionInfo, GuildMetadata, Member, Resource, Toke import Q from './q-module'; import bindWindowButtonEvents from './elements/events-window-buttons'; import bindTextInputEvents from './elements/events-text-input'; -import bindConnectionEvents from './elements/events-connection'; import bindAddGuildTitleEvents from './elements/events-guild-title'; import bindAddGuildEvents from './elements/events-add-guild'; import PersonalDB from './personal-db'; @@ -74,7 +73,6 @@ window.addEventListener('DOMContentLoaded', () => { bindWindowButtonEvents(q); bindTextInputEvents(document, q, ui); - bindConnectionEvents(document, q, ui); bindAddGuildTitleEvents(document, q, ui); bindAddGuildEvents(document, q, ui, guildsManager); diff --git a/src/client/webapp/styles/connection.scss b/src/client/webapp/styles/connection.scss index 65d3e1c..f9b4441 100644 --- a/src/client/webapp/styles/connection.scss +++ b/src/client/webapp/styles/connection.scss @@ -1,23 +1,16 @@ @import "theme.scss"; -#connection.hidden { - visibility: hidden; -} - -#connection.member { +.connection-info { background-color: $background-secondary-alt; padding: 8px; cursor: pointer; /* clicking on this brings up the select-status context menu */ -} -#connection.member .name { - width: calc(224px - 40px); -} + .name { + width: calc(224px - 40px); + font-weight: 600; + } -#member-name.name { - font-weight: 600; -} - -#member-status-circle.status-circle { - border-color: $background-secondary-alt; + .status-circle { + border-color: $background-secondary-alt; + } } diff --git a/src/client/webapp/styles/contexts.scss b/src/client/webapp/styles/contexts.scss index a29527d..98e020b 100644 --- a/src/client/webapp/styles/contexts.scss +++ b/src/client/webapp/styles/contexts.scss @@ -5,6 +5,12 @@ .context { position: fixed; + // Since useEffect gets called after the element is rendered, hide it until + // it gets aligned + &.react:not(.aligned) { + visibility: hidden; + } + .menu { min-width: 180px; box-sizing: border-box; diff --git a/src/client/webapp/ui.ts b/src/client/webapp/ui.ts index b1bab91..8e4146b 100644 --- a/src/client/webapp/ui.ts +++ b/src/client/webapp/ui.ts @@ -137,26 +137,10 @@ export default class UI { await this.lockConnection(guild, () => { this.activeConnection = connection; - this.q.$('#connection').className = 'member ' + connection.status; - this.q.$('#member-name').innerText = connection.displayName; - this.q.$('#member-status-text').innerText = connection.status; - this.q.$('#guild').className = ''; for (const privilege of connection.privileges) { this.q.$('#guild').classList.add('privilege-' + privilege); } - - if (connection.avatarResourceId) { - (async () => { - // Update avatar - if (this.activeGuild === null || this.activeGuild.id !== guild.id) return; - const src = await ElementsUtil.getImageSrcFromResourceFailSoftly(guild, connection.avatarResourceId); - if (this.activeGuild === null || this.activeGuild.id !== guild.id) return; - (this.q.$('#member-avatar') as HTMLImageElement).src = src; - })(); - } else { - (this.q.$('#member-avatar') as HTMLImageElement).src = './img/loading.svg'; - } }); }