react guild title

This commit is contained in:
Michael Peters 2021-12-25 12:58:20 -06:00
parent 80c2a352da
commit 7fde5e70cf
15 changed files with 147 additions and 125 deletions

View File

@ -36,7 +36,7 @@ export default function createChannel(document: Document, q: Q, ui: UI, guild: C
if (modifyContextElement.parentElement) {
modifyContextElement.parentElement.removeChild(modifyContextElement);
}
ElementsUtil.presentReactOverlay(document, <ChannelOverlay document={document} guild={guild} channel={channel} />);
ElementsUtil.presentReactOverlay(document, <ChannelOverlay guild={guild} channel={channel} />);
});
q.$$$(element, '.modify').addEventListener('mouseenter', () => {

View File

@ -1,66 +0,0 @@
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 ElementsUtil from './require/elements-util.js';
import BaseElements from './require/base-elements.js';
import Q from '../q-module';
import UI from '../ui';
import CombinedGuild from '../guild-combined';
import React from 'react';
import ReactHelper from './require/react-helper';
import GuildSettingsOverlay from './overlays/overlay-guild-settings';
import ChannelOverlay from './overlays/overlay-channel';
export default function createGuildTitleContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild): Element {
if (ui.activeConnection === null) {
LOG.warn('no active connection when creating guild title context menu');
return ReactHelper.createElementFromJSX(<div></div>);
}
const menuItems: JSX.Element[] = [];
if (ui.activeConnection.privileges.includes('modify_profile')) {
menuItems.push(
<div key="guild-settings" className="item guild-settings">
<div className="icon">{BaseElements.COG}</div>
<div>Guild Settings</div>
</div>
);
}
if (ui.activeConnection.privileges.includes('modify_channels')) {
if (ui.activeConnection.privileges.includes('modify_profile')) {
menuItems.push(<div key="spacer-1" className="item-spacer"></div>);
}
menuItems.push(
<div key="create-channel" className="item create-channel">
<div className="icon">{BaseElements.CREATE}</div>
<div>Create Channel</div>
</div>
);
}
const element = BaseElements.createContextMenu(document, (
<div className="guild-title-context">{menuItems}</div>
));
if (ui.activeConnection.privileges.includes('modify_profile')) {
q.$$$(element, '.item.guild-settings').addEventListener('click', async () => {
element.removeSelf();
ElementsUtil.presentReactOverlay(document, <GuildSettingsOverlay guild={guild} />);
});
}
if (ui.activeConnection.privileges.includes('modify_channels')) {
q.$$$(element, '.item.create-channel').addEventListener('click', () => {
element.removeSelf();
ElementsUtil.presentReactOverlay(document, <ChannelOverlay document={document} guild={guild} />);
});
}
return element;
}

View File

@ -5,14 +5,14 @@ import PersonalizeOverlay from '../overlays/overlay-personalize';
import ElementsUtil from '../require/elements-util';
import ContextMenu from './components/context-menu';
export interface ConnectionInfoContextProps {
export interface ConnectionInfoContextMenuProps {
guild: CombinedGuild;
selfMember: Member;
relativeToRef: RefObject<HTMLElement | null>;
close: () => void;
}
const ConnectionInfoContext: FC<ConnectionInfoContextProps> = (props: ConnectionInfoContextProps) => {
const ConnectionInfoContextMenu: FC<ConnectionInfoContextMenuProps> = (props: ConnectionInfoContextMenuProps) => {
const { guild, selfMember, relativeToRef, close } = props;
const setSelfStatus = useCallback(async (status: string) => {
@ -56,4 +56,4 @@ const ConnectionInfoContext: FC<ConnectionInfoContextProps> = (props: Connection
);
}
export default ConnectionInfoContext;
export default ConnectionInfoContextMenu;

View File

@ -0,0 +1,64 @@
import React, { FC, RefObject, useCallback, useMemo } from 'react';
import { GuildMetadata, Member } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import ChannelOverlay from '../overlays/overlay-channel';
import GuildSettingsOverlay from '../overlays/overlay-guild-settings';
import BaseElements from '../require/base-elements';
import ElementsUtil, { IAlignment } from '../require/elements-util';
import ContextMenu from './components/context-menu';
export interface GuildTitleContextMenuProps {
alignment: IAlignment;
close: () => void;
relativeToRef: RefObject<HTMLElement>
guild: CombinedGuild;
guildMeta: GuildMetadata;
selfMember: Member;
}
const GuildTitleContextMenu: FC<GuildTitleContextMenuProps> = (props: GuildTitleContextMenuProps) => {
const { alignment, close, relativeToRef, guild, guildMeta, selfMember } = props;
const openGuildSettings = useCallback(() => {
close();
ElementsUtil.presentReactOverlay(document, <GuildSettingsOverlay guild={guild} guildMeta={guildMeta} />);
}, [ guild, guildMeta, close ]);
const openCreateChannel = useCallback(() => {
close();
ElementsUtil.presentReactOverlay(document, <ChannelOverlay guild={guild} />);
}, [ guild, close ]);
const guildSettingsElement = useMemo(() => {
if (!selfMember.privileges.includes('modify_profile')) return null;
return (
<div className="item guild-settings" onClick={openGuildSettings}>
<div className="icon">{BaseElements.COG}</div>
<div>Guild Settings</div>
</div>
);
}, [ selfMember, openGuildSettings ]);
const createChannelElement = useMemo(() => {
if (!selfMember.privileges.includes('modify_channels')) return null;
return (
<div className="item create-channel" onClick={openCreateChannel}>
<div className="icon">{BaseElements.CREATE}</div>
<div>Create Channel</div>
</div>
);
}, [ selfMember, openCreateChannel ]);
if (guildSettingsElement === null && createChannelElement === null) return null;
return (
<ContextMenu alignment={alignment} close={close} relativeToRef={relativeToRef}>
<div className="guild-title-context-menu">
{guildSettingsElement}
{createChannelElement}
</div>
</ContextMenu>
);
}
export default GuildTitleContextMenu;

View File

@ -1,20 +0,0 @@
import Q from '../q-module';
import UI from '../ui';
import createGuildTitleContextMenu from './context-menu-guild-title';
import ElementsUtil from './require/elements-util';
export default function bindAddGuildTitleEvents(document: Document, q: Q, ui: UI) {
q.$('#guild-name-container').addEventListener('click', () => {
if (ui.activeConnection === null) return;
if (ui.activeGuild === null) return;
if (!ui.activeGuild.isSocketVerified()) return;
if (
!ui.activeConnection.privileges.includes('modify_profile') &&
!ui.activeConnection.privileges.includes('modify_members')
) return;
const contextMenu = createGuildTitleContextMenu(document, q, ui, ui.activeGuild) as HTMLElement;
document.body.appendChild(contextMenu);
ElementsUtil.alignContextElement(contextMenu, q.$('#guild-name-container'), { top: 'bottom', centerX: 'centerX' });
});
}

View File

@ -7,6 +7,7 @@ import MemberList from "./lists/member-list";
import MessageList from './lists/message-list';
import ChannelTitle from './sections/channel-title';
import ConnectionInfo from './sections/connection-info';
import GuildTitle from './sections/guild-title';
export function mountBaseComponents() {
// guild-list
@ -14,6 +15,10 @@ export function mountBaseComponents() {
}
export function mountGuildComponents(q: Q, guild: CombinedGuild) {
// guild title
ElementsUtil.unmountReactComponent(q.$('.guild-title-anchor'));
ElementsUtil.mountReactComponent(q.$('.guild-title-anchor'), <GuildTitle guild={guild} />);
// connection info
ElementsUtil.unmountReactComponent(q.$('.connection-anchor'));
ElementsUtil.mountReactComponent(q.$('.connection-anchor'), <ConnectionInfo guild={guild} />);

View File

@ -15,12 +15,11 @@ import ReactHelper from '../require/react-helper';
import Button from '../components/button';
export interface ChannelOverlayProps {
document: Document;
guild: CombinedGuild;
channel?: Channel;
}
const ChannelOverlay: FC<ChannelOverlayProps> = (props: ChannelOverlayProps) => {
const { document, guild, channel } = props;
const { guild, channel } = props;
const nameInputRef = createRef<HTMLInputElement>();

View File

@ -3,20 +3,19 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import React, { FC, useEffect, useMemo, useState } from "react";
import React, { FC, useEffect, useState } from "react";
import CombinedGuild from "../../guild-combined";
import ChoicesControl from "../components/control-choices";
import GuildInvitesDisplay from "../displays/display-guild-invites";
import GuildOverviewDisplay from "../displays/display-guild-overview";
import GuildSubscriptions from "../require/guild-subscriptions";
import { GuildMetadata } from '../../data-types';
export interface GuildSettingsOverlayProps {
guild: CombinedGuild;
guildMeta: GuildMetadata;
}
const GuildSettingsOverlay: FC<GuildSettingsOverlayProps> = (props: GuildSettingsOverlayProps) => {
const { guild } = props;
const [ guildMeta, guildMetaError ] = GuildSubscriptions.useGuildMetadataSubscription(guild);
const { guild, guildMeta } = props;
const [ selectedId, setSelectedId ] = useState<string>('overview');
const [ display, setDisplay ] = useState<JSX.Element>();
@ -27,13 +26,9 @@ const GuildSettingsOverlay: FC<GuildSettingsOverlayProps> = (props: GuildSetting
if (selectedId === 'invites' ) setDisplay(<GuildInvitesDisplay guild={guild} />);
}, [ selectedId ]);
const guildNameText = useMemo(() => {
return guildMetaError ? 'metadata error' : guildMeta?.name ?? 'loading...';
}, [ guildMeta, guildMetaError ])
return (
<div className="content display-swapper guild-settings">
<ChoicesControl title={guildNameText} selectedId={selectedId} setSelectedId={setSelectedId} choices={[
<ChoicesControl title={guildMeta.name} selectedId={selectedId} setSelectedId={setSelectedId} choices={[
{ id: 'overview', display: 'Overview' },
{ id: 'roles', display: 'Roles' },
{ id: 'invites', display: 'Invites' },

View File

@ -6,9 +6,9 @@ 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';
import ConnectionInfoContextMenu from '../context-menus/context-menu-connection-info';
export interface ConnectionInfoProps {
guild: CombinedGuild;
@ -19,8 +19,6 @@ const ConnectionInfo: FC<ConnectionInfoProps> = (props: ConnectionInfoProps) =>
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);
@ -40,7 +38,7 @@ const ConnectionInfo: FC<ConnectionInfoProps> = (props: ConnectionInfoProps) =>
const contextMenu = useMemo(() => {
if (!selfMember) return null;
return <ConnectionInfoContext guild={guild} selfMember={selfMember} relativeToRef={rootRef} close={() => { console.log('close'); setContextMenuOpen(false); }} />
return <ConnectionInfoContextMenu guild={guild} selfMember={selfMember} relativeToRef={rootRef} close={() => { setContextMenuOpen(false); }} />
}, [ guild, selfMember, rootRef ]);
const toggleContextMenu = useCallback(() => {

View File

@ -0,0 +1,50 @@
import React, { FC, useCallback, useMemo, useRef, useState } from 'react';
import CombinedGuild from '../../guild-combined';
import GuildTitleContextMenu from '../context-menus/context-menu-guild-title';
import GuildSubscriptions from '../require/guild-subscriptions';
export interface GuildTitleProps {
guild: CombinedGuild;
}
const GuildTitle: FC<GuildTitleProps> = (props: GuildTitleProps) => {
const { guild } = props;
const rootRef = useRef<HTMLDivElement>(null);
const [ contextMenuOpen, setContextMenuOpen ] = useState<boolean>(false);
// TODO: Handle fetch error
const [ guildMeta, fetchError ] = GuildSubscriptions.useGuildMetadataSubscription(guild);
const [ selfMember ] = GuildSubscriptions.useSelfMemberSubscription(guild);
const alignment = useMemo(() => {
return { top: 'bottom', centerX: 'centerX' }
}, []);
const contextMenu = useMemo(() => {
if (!guildMeta) return null;
if (!selfMember) return null;
return (
<GuildTitleContextMenu
alignment={alignment} relativeToRef={rootRef} close={() => { setContextMenuOpen(false); }}
guild={guild} guildMeta={guildMeta} selfMember={selfMember}
/>
);
}, [ guild, guildMeta, selfMember, rootRef ]);
const toggleContextMenu = useCallback(() => {
setContextMenuOpen(oldContextMenuOpen => !!contextMenu && !oldContextMenuOpen);
}, [ contextMenu ]);
return (
<div className="guild-title" ref={rootRef}>
<div className="guild-name-container" onClick={toggleContextMenu}>
<span className="guild-name">{guildMeta?.name ?? null}</span>
</div>
{contextMenuOpen ? contextMenu : null}
</div>
);
}
export default GuildTitle;

View File

@ -44,7 +44,7 @@
</div>
<div id="guild">
<div id="guild-sidebar">
<div id="guild-name-container"><span id="guild-name"></span></div>
<div class="guild-title-anchor"></div>
<div id="channel-list"></div>
<div class="connection-anchor"></div>
</div>

View File

@ -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 bindAddGuildTitleEvents from './elements/events-guild-title';
import bindAddGuildEvents from './elements/events-add-guild';
import PersonalDB from './personal-db';
import MessageRAMCache from './message-ram-cache';
@ -73,7 +72,6 @@ window.addEventListener('DOMContentLoaded', () => {
bindWindowButtonEvents(q);
bindTextInputEvents(document, q, ui);
bindAddGuildTitleEvents(document, q, ui);
bindAddGuildEvents(document, q, ui, guildsManager);
LOG.silly('events bound');

View File

@ -53,8 +53,8 @@
margin-right: 8px;
}
.guild-title-context .item .icon img,
.guild-title-context .item .icon svg {
.guild-title-context-menu .item .icon img,
.guild-title-context-menu .item .icon svg {
width: 16px;
height: 16px;
}

View File

@ -14,25 +14,25 @@
border-top-left-radius: 8px;
}
#guild-name-container {
// TODO: just do this with inline styles
.privilege-modify_profile .guild-name-container,
.privilege-modify_members .guild-name-container {
cursor: pointer;
}
.guild-name-container {
padding: 0 16px;
height: 48px;
font-weight: 600;
display: flex;
align-items: center;
cursor: pointer;
color: $header-primary;
border-bottom: 1px solid $background-secondary-alt;
}
#guild-name {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#guild.privilege-modify_profile #guild-name,
#guild.privilege-modify_members #guild-name {
cursor: pointer;
.guild-name {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}

View File

@ -180,7 +180,6 @@ export default class UI {
public async updateGuildName(guild: CombinedGuild, name: string): Promise<void>{
await this.lockGuildName(guild, () => {
this.q.$('#guild-name').innerText = name;
const baseElement = this.q.$('#guild-list .guild[data-id="' + guild.id + '"]');
baseElement.setAttribute('meta-name', name);
});