connection info and context menus
This commit is contained in:
parent
a47804d980
commit
80c2a352da
@ -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[] = [
|
||||
<div key="personalize" className="item personalize">
|
||||
<div className="icon"><img src="./img/pencil-icon.png"></img></div>
|
||||
<div>Personalize</div>
|
||||
</div>,
|
||||
<div key="spacer-1" className="item-spacer"></div>
|
||||
];
|
||||
content = content.concat(statuses.map(status => (
|
||||
<div key={status} className={'item ' + status}>
|
||||
<div className="status-circle"></div>
|
||||
<div className="status-text">{status}</div>
|
||||
</div>
|
||||
)));
|
||||
const element = BaseElements.createContextMenu(document, <div className="member-context">{content}</div>);
|
||||
|
||||
q.$$$(element, '.personalize').addEventListener('click', async () => {
|
||||
element.removeSelf();
|
||||
if (ui.activeConnection === null) return;
|
||||
ElementsUtil.presentReactOverlay(document, <PersonalizeOverlay document={document} guild={guild} connection={ui.activeConnection} />);
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
@ -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<HTMLElement | null>;
|
||||
alignment: IAlignment;
|
||||
children: ReactNode;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
const ContextMenu: FC<ContextMenuProps> = (props: ContextMenuProps) => {
|
||||
const { relativeToRef, alignment, children, close } = props;
|
||||
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [ aligned, setAligned ] = useState<boolean>(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 (
|
||||
<div ref={rootRef} className={contextClass}>
|
||||
<div className="menu">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ContextMenu;
|
@ -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<HTMLElement | null>;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
const ConnectionInfoContext: FC<ConnectionInfoContextProps> = (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 (
|
||||
<div key={status} className={'item ' + status} onClick={() => { setSelfStatus(status); close(); }}>
|
||||
<div className="status-circle"></div>
|
||||
<div className="status-text">{status}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}, [ 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, <PersonalizeOverlay document={document} guild={guild} selfMember={selfMember} />);
|
||||
}, [ guild, selfMember, close ]);
|
||||
|
||||
const alignment = useMemo(() => {
|
||||
return { bottom: 'top', centerX: 'centerX' }
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ContextMenu relativeToRef={relativeToRef} alignment={alignment} close={close}>
|
||||
<div className="member-context">
|
||||
<div onClick={openPersonalize} className="item">
|
||||
<div className="icon"><img src="./img/pencil-icon.png" /></div>
|
||||
<div>Personalize</div>
|
||||
</div>
|
||||
<div className="item-spacer" />
|
||||
{statusElements}
|
||||
</div>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConnectionInfoContext;
|
@ -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<GuildInvitesDisplayProps> = (props: GuildInvitesDi
|
||||
const [ expiresFromNow, setExpiresFromNow ] = useState<Duration | null>(moment.duration(1, 'day'));
|
||||
const [ expiresFromNowText, setExpiresFromNowText ] = useState<string>('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') {
|
||||
|
@ -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' });
|
||||
});
|
||||
}
|
@ -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<MemberProps> = (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 ]);
|
||||
|
||||
|
@ -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<PreviewImageElementProps> = (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
|
||||
|
@ -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'), <ConnectionInfo guild={guild} />);
|
||||
|
||||
// member-list
|
||||
ElementsUtil.unmountReactComponent(q.$('.member-list-anchor'));
|
||||
ElementsUtil.mountReactComponent(q.$('.member-list-anchor'), <MemberList guild={guild} />);
|
||||
|
@ -26,6 +26,7 @@ const ImageOverlay: FC<ImageOverlayProps> = (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',
|
||||
|
@ -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<PersonalizeOverlayProps> = (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<HTMLInputElement>();
|
||||
|
||||
const [ savedDisplayName, setSavedDisplayName ] = useState<string>(connection.displayName);
|
||||
const [ savedDisplayName, setSavedDisplayName ] = useState<string>(selfMember.displayName);
|
||||
const [ savedAvatarBuff, setSavedAvatarBuff ] = useState<Buffer | null>(null);
|
||||
|
||||
const [ displayName, setDisplayName ] = useState<string>(connection.displayName);
|
||||
const [ displayName, setDisplayName ] = useState<string>(selfMember.displayName);
|
||||
const [ avatarBuff, setAvatarBuff ] = useState<Buffer | null>(null);
|
||||
|
||||
const [ displayNameInputValid, setDisplayNameInputValid ] = useState<boolean>(false);
|
||||
|
@ -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(
|
||||
<div className="context">
|
||||
<div className="menu">{content}</div>
|
||||
@ -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;
|
||||
}
|
||||
|
@ -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<void> {
|
||||
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<string> {
|
||||
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<string> {
|
||||
if (resourceId === null) {
|
||||
return './img/loading.svg';
|
||||
|
@ -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');
|
||||
|
@ -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<HTMLElement>, 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 ]);
|
||||
}
|
||||
}
|
||||
|
58
src/client/webapp/elements/sections/connection-info.tsx
Normal file
58
src/client/webapp/elements/sections/connection-info.tsx
Normal file
@ -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<ConnectionInfoProps> = (props: ConnectionInfoProps) => {
|
||||
const { guild } = props;
|
||||
|
||||
const rootRef = useRef<HTMLDivElement>(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<boolean>(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 <ConnectionInfoContext guild={guild} selfMember={selfMember} relativeToRef={rootRef} close={() => { console.log('close'); setContextMenuOpen(false); }} />
|
||||
}, [ guild, selfMember, rootRef ]);
|
||||
|
||||
const toggleContextMenu = useCallback(() => {
|
||||
setContextMenuOpen(oldContextMenuOpen => !!contextMenu && !oldContextMenuOpen);
|
||||
}, [ contextMenu ]);
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className="connection-info">
|
||||
<div onClick={toggleContextMenu}><MemberElement guild={guild} member={displayMember} /></div>
|
||||
{contextMenuOpen ? contextMenu : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConnectionInfo;
|
@ -46,16 +46,7 @@
|
||||
<div id="guild-sidebar">
|
||||
<div id="guild-name-container"><span id="guild-name"></span></div>
|
||||
<div id="channel-list"></div>
|
||||
<div id="connection" class="hidden member">
|
||||
<div class="icon">
|
||||
<img id="member-avatar" class="avatar" src="./img/loading.svg" alt="Loading...">
|
||||
<div id="member-status-circle" class="status-circle offline"></div>
|
||||
</div>
|
||||
<div class="text">
|
||||
<div id="member-name" class="name">Connecting...</div>
|
||||
<div id="member-status-text" class="status-text">Connecting...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="connection-anchor"></div>
|
||||
</div>
|
||||
<div id="channel">
|
||||
<div class="channel-title-anchor"></div>
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user