remove old members stuff

This commit is contained in:
Michael Peters 2021-12-20 23:24:05 -06:00
parent 3c14ecb6ec
commit 971b4c4d79
19 changed files with 212 additions and 255 deletions

View File

@ -22,21 +22,6 @@ export default class Actions {
ui.setActiveConnection(guild, { id: null, avatarResourceId: null, displayName: 'Error', status: 'Error', privileges: [], roleName: null, roleColor: null, rolePriority: null });
}
}
static async fetchAndUpdateMembers(q: Q, ui: UI, guild: CombinedGuild) {
await Util.withPotentialErrorWarnOnCancel(q, {
taskFunc: async () => {
if (ui.activeGuild === null || ui.activeGuild.id !== guild.id) return;
const members = await guild.fetchMembers();
await ui.setMembers(guild, members);
},
errorIndicatorAddFunc: async (errorIndicatorElement) => {
await ui.setMembersErrorIndicator(guild, errorIndicatorElement);
},
errorContainer: q.$('#guild-members'),
errorMessage: 'Error loading members'
});
}
static async fetchAndUpdateChannels(q: Q, ui: UI, guild: CombinedGuild) {
await Util.withPotentialErrorWarnOnCancel(q, {

View File

@ -59,6 +59,38 @@ export class Member implements WithEquals<Member> {
this.privileges.join(',') === other.privileges.join(',') // TODO: sort these first
);
}
// Ordered by online/offline
// Ordered by role priority
// Ordered by status
// Ordered by display name
// Ordered by id
public static sortForList(a: Member, b: Member) {
const statusOrder = new Map();
statusOrder.set('online', 0);
statusOrder.set('away', 1);
statusOrder.set('busy', 2);
statusOrder.set('offline', 3);
statusOrder.set('invisible', 3); // this status is only shown in the case of the current member.
statusOrder.set('unknown', 100);
const onlineCmp = (a.status === 'offline' ? 1 : 0) - (b.status === 'offline' ? 1 : 0);
if (onlineCmp !== 0) return onlineCmp;
const priorityCmp = (a.rolePriority ?? 100) - (b.rolePriority ?? 100);
if (priorityCmp !== 0) return priorityCmp;
const statusCmp = (statusOrder.get(a.status) ?? 100) - (statusOrder.get(b.status) ?? 100);
if (statusCmp !== 0) return statusCmp;
const nameCmp = strcmp(a.displayName, b.displayName);
if (nameCmp !== 0) return nameCmp;
const idCmp = strcmp(a.id, b.id);
if (idCmp !== 0) return idCmp;
return 0;
}
}
export class Channel implements WithEquals<Channel> {

View File

@ -3,7 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import React, { FC, useEffect, useMemo, useState } from 'react';
import CombinedGuild from '../../guild-combined';
import Display from '../components/display';
import InvitePreview from '../components/invite-preview';
@ -29,15 +29,16 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
const [ tokens, tokensError ] = GuildSubscriptions.useTokensSubscription(guild);
const [ guildMeta, guildMetaError ] = GuildSubscriptions.useGuildMetadataSubscription(guild);
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 [ expiresFromNow, setExpiresFromNow ] = useState<Duration | null>(moment.duration(1, 'day'));
const [ expiresFromNowText, setExpiresFromNowText ] = useState<string>('1 day');
useEffect(() => {
if (expiresFromNowText === 'never') {
setExpiresFromNow(null);
@ -48,24 +49,29 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
}
}, [ expiresFromNowText ]);
const [ tokenResult, tokenError, tokenButtonText, tokenButtonShaking, tokenButtonCallback ] = ReactHelper.useAsyncButtonSubscription(
async () => await guild.requestDoCreateToken(expiresFromNowText === 'never' ? null : expiresFromNowText),
{ start: 'Create Token', pending: 'Creating...', error: 'Try Again', done: 'Create Token' },
[ guild, expiresFromNowText ]
const [ createTokenFunc, tokenButtonText, tokenButtonShaking, createTokenFailMessage ] = ReactHelper.useSubmitButton(
async () => {
try {
const createdToken = await guild.requestDoCreateToken(expiresFromNowText === 'never' ? null : expiresFromNowText)
return { result: createdToken, errorMessage: null };
} catch (e: unknown) {
LOG.error('error creating token', e);
return { result: null, errorMessage: 'Error creating token' };
}
},
[ guild, expiresFromNowText ],
{ start: 'Create Token', done: 'Create Another Token' }
);
const createToken = useCallback(async () => {
await guild.requestDoCreateToken(expiresFromNowText); // note: the text, NOT the duration. The server uses PostgreSQL interval conversion
}, [ expiresFromNowText ]);
const errorMessage = useMemo(() => {
if (guildMetaError) return 'Unable to load guild metadata';
if (tokenError) return 'Unable to create token';
if (createTokenFailMessage) return createTokenFailMessage;
return null;
}, [ guildMetaError ]);
const tokenElements = useMemo(() => {
if (tokensError) {
// TODO: Try Again
return <div className="tokens-failed">Unable to load tokens</div>;
}
return tokens?.map((token: Token) => {
@ -107,7 +113,7 @@ const GuildInvitesDisplay: FC<GuildInvitesDisplayProps> = (props: GuildInvitesDi
{ value: '1 month', display: 'a Month' },
{ value: 'never', display: 'Never' },
]} />
<div><Button shaking={tokenButtonShaking} onClick={tokenButtonCallback}>{tokenButtonText}</Button></div>
<div><Button shaking={tokenButtonShaking} onClick={createTokenFunc}>{tokenButtonText}</Button></div>
</div>
<InvitePreview
name={guildMeta?.name ?? ''} iconSrc={iconSrc}

View File

@ -100,11 +100,6 @@ export default function createGuildListGuild(document: Document, q: Q, ui: UI, g
(async () => {
await Actions.fetchAndUpdateChannels(q, ui, guild);
})();
// Guild Members
(async () => {
await Actions.fetchAndUpdateMembers(q, ui, guild);
})();
});
element.addEventListener('contextmenu', (e) => {

View File

@ -0,0 +1,37 @@
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';
export interface MemberProps {
guild: CombinedGuild;
member: Member;
}
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 nameStyle = useMemo(() => member.roleColor ? { color: member.roleColor } : {}, [ member.roleColor ]);
return (
<div className={'member ' + member.status} data-id={member.id}>
<div className="icon">
<img className="avatar" src={avatarSrc} alt={member.displayName}></img>
<div className="status-circle"></div>
</div>
<div className="text">
<div className="name" style={nameStyle}>{member.displayName}</div>
<div className="status-text">{member.status}</div>
</div>
</div>
);
}
export default MemberElement;

View File

@ -0,0 +1,3 @@
@import "./member-list.scss";
@import "./components/member-element.scss";

View File

@ -0,0 +1,33 @@
@import "../../styles/theme.scss";
.member-list-anchor {
box-sizing: border-box;
flex: none; /* >:| NOT GONNA SHINK BOI */
background-color: $background-secondary;
width: 240px;
height: calc(100vh - 71px);
overflow-y: scroll;
padding: 8px 0 8px 8px;
.member-list {
.member {
background-color: $background-secondary;
padding: 4px 8px;
margin-bottom: 4px;
border-radius: 4px;
}
.member .name {
width: calc(208px - 40px);
}
.member .status-circle {
border-color: $background-secondary;
}
.member:hover {
background-color: $background-modifier-hover;
cursor: pointer;
}
}
}

View File

@ -0,0 +1,31 @@
import React, { FC, useMemo } from 'react';
import { Member } from '../../data-types';
import CombinedGuild from '../../guild-combined';
import GuildSubscriptions from '../require/guild-subscriptions';
import MemberElement from './components/member-element';
export interface MemberListProps {
guild: CombinedGuild
}
const MemberList: FC<MemberListProps> = (props: MemberListProps) => {
const { guild } = props;
const [ members, fetchError ] = GuildSubscriptions.useMembersSubscription(guild);
const memberElements = useMemo(() => {
if (fetchError) {
// TODO: Try Again
return <div className="members-failed">Unable to load members</div>
}
return members?.map((member: Member) => <MemberElement key={member.id} guild={guild} member={member} />);
}, [ members, fetchError ]);
return (
<div className="member-list">
{memberElements}
</div>
);
};
export default MemberList;

View File

@ -1,28 +0,0 @@
import React from "react";
import { Member } from "../data-types";
import CombinedGuild from "../guild-combined";
import Q from "../q-module";
import ElementsUtil from "./require/elements-util";
import ReactHelper from "./require/react-helper";
export default function createMember(q: Q, guild: CombinedGuild, member: Member): Element {
const nameStyle = member.roleColor ? { color: member.roleColor } : {};
const element = ReactHelper.createElementFromJSX(
<div className={'member ' + member.status} data-id={member.id}>
<div className="icon">
<img className="avatar" src="./img/loading.svg" alt={member.displayName}></img>
<div className="status-circle"></div>
</div>
<div className="text">
<div className="name" style={nameStyle}>{member.displayName}</div>
<div className="status-text">{member.status}</div>
</div>
</div>
);
(async () => {
(q.$$$(element, 'img.avatar') as HTMLImageElement).src =
await ElementsUtil.getImageSrcFromResourceFailSoftly(guild, member.avatarResourceId);
})();
return element;
}

View File

@ -0,0 +1,22 @@
import React from 'react';
import { Channel } from "../data-types";
import CombinedGuild from "../guild-combined";
import Q from "../q-module";
import ElementsUtil from "./require/elements-util";
import MemberList from "./lists/member-list";
export function mountBaseComponents() {
// guild-list
}
export function mountGuildComponents(q: Q, guild: CombinedGuild) {
// member-list
ElementsUtil.unmountReactComponent(q.$('.member-list-anchor'));
ElementsUtil.mountReactComponent(q.$('.member-list-anchor'), <MemberList guild={guild} />);
// channel-list
}
export function mountGuildChannelComponents(guild: CombinedGuild, channel: Channel) {
// message-list
}

View File

@ -382,6 +382,14 @@ export default class ElementsUtil {
}
}
static mountReactComponent(element: Element, component: JSX.Element) {
ReactDOM.render(component, element);
}
static unmountReactComponent(element: Element) {
ReactDOM.unmountComponentAtNode(element);
}
static presentReactOverlay(document: Document, content: JSX.Element) {
const overlay = <Overlay document={document}>{content}</Overlay>;
ReactDOM.render(overlay, document.querySelector('#react-overlays'));

View File

@ -3,7 +3,7 @@ const electronConsole = electronRemote.getGlobal('console') as Console;
import Logger from '../../../../logger/logger';
const LOG = Logger.create(__filename, electronConsole);
import { Changes, GuildMetadata, Resource } from "../../data-types";
import { Changes, GuildMetadata, Member, Resource } from "../../data-types";
import CombinedGuild from "../../guild-combined";
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@ -340,6 +340,23 @@ export default class GuildSubscriptions {
}, fetchChannelsFunc);
}
static useMembersSubscription(guild: CombinedGuild) {
const fetchMembersFunc = useCallback(async () => {
return await guild.fetchMembers();
}, [ guild ]);
return GuildSubscriptions.useMultipleGuildSubscription<Member, 'new-members', 'update-members', 'remove-members', 'conflict-members'>(guild, {
newEventName: 'new-members',
newEventArgsMap: (members: Member[]) => members,
updatedEventName: 'update-members',
updatedEventArgsMap: (updatedMembers: Member[]) => updatedMembers,
removedEventName: 'remove-members',
removedEventArgsMap: (removedMembers: Member[]) => removedMembers,
conflictEventName: 'conflict-members',
conflictEventArgsMap: (changesType: AutoVerifierChangesType, changes: Changes<Member>) => changes,
sortFunc: Member.sortForList
}, fetchMembersFunc);
}
static useTokensSubscription(guild: CombinedGuild) {
const fetchTokensFunc = useCallback(async () => {
//LOG.silly('fetching tokens for subscription');

View File

@ -8,8 +8,6 @@ import ReactDOMServer from "react-dom/server";
import { ShouldNeverHappenError } from "../../data-types";
import Util from '../../util';
export class ExpectedError extends Error {}
// Helper function so we can use JSX before fully committing to React
export default class ReactHelper {
@ -122,51 +120,4 @@ export default class ReactHelper {
return [ submitFunc, buttonText, buttonShaking, errorMessage, result ];
}
static useAsyncButtonSubscription<T>(
actionFunc: () => Promise<T>,
stateText: { start: string, pending: string, error: string, done: string },
deps: DependencyList
): [ result: T | null, error: unknown | null, text: string, shaking: boolean, callback: () => void ] {
const isMounted = ReactHelper.useIsMountedRef();
const [ result, setResult ] = useState<T | null>(null);
const [ error, setError ] = useState<unknown | null>(null);
const [ pending, setPending ] = useState<boolean>(false);
const [ complete, setComplete ] = useState<boolean>(false);
const [ shaking, setShaking ] = useState<boolean>(false);
const text = useMemo(() => {
if (error) return stateText.error;
if (pending) return stateText.pending;
if (complete) return stateText.done;
return stateText.start;
}, [ error, pending, complete ]);
const callback = useCallback(async () => {
if (pending) return;
setError(null);
setPending(true);
try {
const value = await actionFunc();
if (!isMounted.current) return;
setResult(value);
setComplete(true);
setError(null);
setPending(false);
} catch (e: unknown) {
LOG.error('unable to perform async button subscription');
if (!isMounted.current) return;
setError(e);
setShaking(true);
await Util.sleep(400);
if (!isMounted.current) return;
setShaking(false);
setPending(false);
}
}, [ ...deps, pending ]);
return [ result, error, text, shaking, callback ];
}
}

View File

@ -87,7 +87,8 @@
</div>
</div>
</div>
<div id="guild-members"></div>
<div id="guild-members"></div><!-- TODO: guild-members-react -->
<div class="member-list-anchor"></div>
</div>
</div>
</div>

View File

@ -96,9 +96,6 @@ window.addEventListener('DOMContentLoaded', () => {
(async () => { // update connection info
await Actions.fetchAndUpdateConnection(ui, guild);
})();
(async () => { // refresh members list
await Actions.fetchAndUpdateMembers(q, ui, guild);
})();
(async () => { // refresh channels list
await Actions.fetchAndUpdateChannels(q, ui, guild);
})();
@ -123,9 +120,6 @@ window.addEventListener('DOMContentLoaded', () => {
(async () => {
await Actions.fetchAndUpdateConnection(ui, guild);
})();
(async () => {
await Actions.fetchAndUpdateMembers(q, ui, guild);
})();
});
// Change Events
@ -147,14 +141,8 @@ window.addEventListener('DOMContentLoaded', () => {
}
});
guildsManager.on('remove-members', async (guild: CombinedGuild, members: Member[]) => {
LOG.debug(members.length + ' removed members');
await ui.deleteMembers(guild, members);
});
guildsManager.on('update-members', async (guild: CombinedGuild, updatedMembers: Member[]) => {
LOG.debug(updatedMembers.length + ' updated members g#' + guild.id);
await ui.updateMembers(guild, updatedMembers);
if (
ui.activeConnection !== null &&
updatedMembers.find(member => member.id === (ui.activeConnection as ConnectionInfo).id)
@ -163,11 +151,6 @@ window.addEventListener('DOMContentLoaded', () => {
}
});
guildsManager.on('new-members', async (guild: CombinedGuild, members: Member[]) => {
LOG.debug(members.length + ' new members');
await ui.addMembers(guild, members);
});
guildsManager.on('remove-channels', async (guild: CombinedGuild, channels: Channel[]) => {
LOG.debug(channels.length + ' removed channels');
await ui.deleteChannels(guild, channels);
@ -229,12 +212,7 @@ window.addEventListener('DOMContentLoaded', () => {
guildsManager.on('conflict-members', async (guild: CombinedGuild, changesType: AutoVerifierChangesType, changes: Changes<Member>) => {
//LOG.debug('members conflict', { changes });
if (changes.deleted.length > 0) await ui.deleteMembers(guild, changes.deleted);
if (changes.added.length > 0) await ui.addMembers(guild, changes.added);
if (changes.updated.length > 0) {
(async () => {
await ui.updateMembers(guild, changes.updated.map(pair => pair.newDataPoint));
})();
(async () => {
LOG.debug('updating conflict members connection...');
// Likely getting called before the ram is updated

View File

@ -1,31 +0,0 @@
@import "theme.scss";
#guild-members {
box-sizing: border-box;
flex: none; /* >:| NOT GONNA SHINK BOI */
background-color: $background-secondary;
width: 240px;
height: calc(100vh - 71px);
overflow-y: scroll;
padding: 8px 0 8px 8px;
}
#guild-members .member {
background-color: $background-secondary;
padding: 4px 8px;
margin-bottom: 4px;
border-radius: 4px;
}
#guild-members .member .name {
width: calc(208px - 40px);
}
#guild-members .member .status-circle {
border-color: $background-secondary;
}
#guild-members .member:hover {
background-color: $background-modifier-hover;
cursor: pointer;
}

View File

@ -4,6 +4,8 @@
@import "../elements/components/components.scss";
@import "../elements/overlays/overlays.scss";
@import "../elements/lists/lists.scss";
@import "buttons.scss";
@import "channel-feed.scss";
@import "channel-list.scss";
@ -18,7 +20,6 @@
@import "overlays.scss";
@import "scrollbars.scss";
@import "guild-list.scss";
@import "guild-members.scss";
@import "guild.scss";
@import "shake.scss";
@import "status-circles.scss";

View File

@ -10,14 +10,14 @@ import ElementsUtil from './elements/require/elements-util';
import Globals from './globals';
import Util from './util';
import CombinedGuild from './guild-combined';
import { Message, Member, Channel, ConnectionInfo, ShouldNeverHappenError } from './data-types';
import { Message, Channel, ConnectionInfo, ShouldNeverHappenError } from './data-types';
import Q from './q-module';
import createGuildListGuild from './elements/guild-list-guild';
import createChannel from './elements/channel';
import createMember from './elements/member';
import GuildsManager from './guilds-manager';
import createMessage from './elements/message';
import { mountGuildComponents } from './elements/mounts';
interface SetMessageProps {
atTop: boolean;
atBottom: boolean;
@ -109,6 +109,7 @@ export default class UI {
const next = this.q.$('#guild-list .guild[data-id="' + guild.id + '"]');
next.classList.add('active');
this.q.$('#guild').setAttribute('data-id', guild.id + '');
mountGuildComponents(this.q, guild);
this.activeGuild = guild;
}
@ -317,91 +318,6 @@ export default class UI {
});
}
public async updateMemberPosition(guild: CombinedGuild, memberElement: Element): Promise<void> {
// TODO: Change 100 to a constant?
const statusOrder = new Map();
statusOrder.set('online', 0);
statusOrder.set('away', 1);
statusOrder.set('busy', 2);
statusOrder.set('offline', 3);
statusOrder.set('invisible', 3); // this status is only shown in the case of the current member.
statusOrder.set('unknown', 100);
this._updatePosition(memberElement, await guild.grabRAMMembersMap(), (a, b) => {
const onlineCmp = (a.status === 'offline' ? 1 : 0) - (b.status === 'offline' ? 1 : 0);
if (onlineCmp != 0) return onlineCmp;
const rolePriorityCmp = (a.rolePriority ?? 100) - (b.rolePriority ?? 100);
if (rolePriorityCmp != 0) return rolePriorityCmp;
const statusCmp = (statusOrder.get(a.status) ?? 100) - (statusOrder.get(b.status) ?? 100);
if (statusCmp != 0) return statusCmp;
const nameCmp = a.displayName.localeCompare(b.displayName);
return nameCmp;
});
}
public async addMembers(guild: CombinedGuild, members: Member[], options?: { clear: boolean }): Promise<void> {
await this.lockMembers(guild, async () => {
if (options?.clear) {
Q.clearChildren(this.q.$('#guild-members'));
}
for (const member of members) {
const element = createMember(this.q, guild, member);
this.q.$('#guild-members').appendChild(element);
await this.updateMemberPosition(guild, element);
}
});
}
public async deleteMembers(guild: CombinedGuild, members: Member[]): Promise<void> {
await this.lockMembers(guild, () => {
for (const member of members) {
const element = this.q.$_('#guild-members .member[data-id="' + member.id + '"]');
element?.parentElement?.removeChild(element);
}
});
}
public async updateMembers(guild: CombinedGuild, updatedMembers: Member[]): Promise<void> {
await this.lockMembers(guild, async () => {
for (const member of updatedMembers) {
const oldElement = this.q.$_('#guild-members .member[data-id="' + member.id + '"]');
if (oldElement) {
const newElement = createMember(this.q, guild, member);
oldElement.parentElement?.replaceChild(newElement, oldElement);
await this.updateMemberPosition(guild, newElement);
}
}
});
// Update the messages too
if (this.activeChannel === null) return;
await this.lockMessages(guild, this.activeChannel, () => {
for (const member of updatedMembers) {
const newStyle = member.roleColor ? 'color: ' + member.roleColor : null;
const newName = member.displayName;
// the extra query selectors may be overkill
for (const messageElement of this.q.$$(`.message[data-member-id="${member.id}"]`)) {
const nameElement = this.q.$$$_(messageElement, '.member-name');
if (nameElement) { // continued messages will still show up but need to be skipped
if (newStyle) nameElement.setAttribute('style', newStyle);
nameElement.innerText = newName;
}
}
}
});
}
public async setMembers(guild: CombinedGuild, members: Member[]): Promise<void> {
await this.addMembers(guild, members, { clear: true });
}
public async setMembersErrorIndicator(guild: CombinedGuild, errorIndicatorElement: Element): Promise<void> {
await this.lockMembers(guild, () => {
Q.clearChildren(this.q.$('#guild-members'));
this.q.$('#guild-members').appendChild(errorIndicatorElement);
});
}
public getTopMessagePair(): { message: Message, element: Element } | null {
const element = this.q.$$('#channel-feed .message')[0];
return element && this.messagePairs.get(element.getAttribute('data-id')) || null;