Using a react poke modal

master
WolverinDEV 2021-03-17 22:36:49 +01:00
parent 081ed060ed
commit 89b63a425d
12 changed files with 326 additions and 110 deletions

View File

@ -18,7 +18,6 @@ import "./static/modal-identity.scss"
import "./static/modal-newcomer.scss"
import "./static/modal-invite.scss"
import "./static/modal-keyselect.scss"
import "./static/modal-poke.scss"
import "./static/modal-query.scss"
import "./static/modal-server.scss"
import "./static/modal-musicmanage.scss"

View File

@ -1,90 +0,0 @@
html:root {
--modal-poke-date: cornflowerblue;
--modal-poke-text: #004d00;
}
:global {
.container-poke {
display: flex!important;;
flex-direction: column!important;;
.container-servers {
display: flex;
flex-direction: column;
justify-content: stretch;
.server {
display: flex;
flex-direction: column;
justify-content: stretch;
.server-name {
margin-top: 5px;
flex-grow: 0;
flex-shrink: 0;
font-weight: bold;
text-decoration: underline;
}
.poke-list {
display: flex;
flex-direction: column;
justify-content: flex-start;
overflow-y: auto;
overflow-x: auto;
.entry {
display: flex;
flex-direction: row;
justify-content: flex-start;
flex-shrink: 0;
flex-grow: 0;
> * {
white-space: nowrap;
}
.date, .user, .text {
margin-right: 5px;
}
.date {
color: var(--modal-poke-date);
}
.text {
color: var(--modal-poke-text);
}
}
}
}
}
.buttons {
display: flex;
flex-direction: row;
justify-content: stretch;
margin-top: 5px;
flex-shrink: 0;
flex-grow: 0;
.spacer {
flex-grow: 1;
flex-shrink: 1;
}
.button-close {
flex-grow: 0;
flex-shrink: 0;
margin-top: 5px;
width: 150px;
float: right;
}
}
}
}

View File

@ -1179,16 +1179,6 @@
</div>
</script>
<script class="jsrender-template" id="tmpl_poke_popup" type="text/html">
<div class="container-poke">
<div class="container-servers"></div>
<div class="buttons">
<div class="spacer"></div>
<button class="btn btn-raised btn-primary button-close">{{tr "Close" /}}</button>
</div>
</div>
</script>
<script class="jsrender-template" id="tmpl_query_create" type="text/html">
<div class="query-create">
<a>{{tr "Set the login name for your Server Query account." /}}</a>

View File

@ -23,6 +23,7 @@ import {ErrorCode} from "../connection/ErrorCode";
import {server_connections} from "tc-shared/ConnectionManager";
import {ChannelEntry} from "tc-shared/tree/Channel";
import {EventClient} from "tc-shared/connectionlog/Definitions";
import {spawnPokeModal} from "tc-shared/ui/modal/poke/Controller";
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
constructor(connection: AbstractServerConnection) {
@ -982,10 +983,11 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
handleNotifyClientPoke(json) {
json = json[0];
spawnPoke(this.connection_handler, {
id: parseInt(json["invokerid"]),
name: json["invokername"],
unique_id: json["invokeruid"]
spawnPokeModal(this.connection_handler, {
clientId: parseInt(json["invokerid"]),
clientName: json["invokername"],
clientUniqueId: json["invokeruid"]
}, json["msg"]);
this.connection_handler.log.log("client.poke.received", {

View File

@ -676,7 +676,7 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
}
}
generate_bbcode() {
generateBBCode() {
return "[url=channel://" + this.channelId + "/" + encodeURIComponent(this.properties.channel_name) + "]" + this.formattedChannelName() + "[/url]";
}

View File

@ -0,0 +1,91 @@
import {createIpcUiVariableProvider, IpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
import {ModalPokeEvents, ModalPokeVariables, PokeRecord} from "tc-shared/ui/modal/poke/Definitions";
import {Registry} from "tc-events";
import {server_connections} from "tc-shared/ConnectionManager";
import {ModalController} from "tc-shared/ui/react-elements/modal/Definitions";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {spawnModal} from "tc-shared/ui/react-elements/modal";
import {guid} from "tc-shared/crypto/uid";
let controllerInstance: PokeController;
class PokeController {
readonly variables: IpcUiVariableProvider<ModalPokeVariables>;
readonly events: Registry<ModalPokeEvents>;
private modalInstance: ModalController;
private registeredEvents: (() => void)[] = [];
private registeredRecords: PokeRecord[] = [];
constructor() {
this.variables = createIpcUiVariableProvider();
this.events = new Registry<ModalPokeEvents>();
this.variables.setVariableProvider("pokeList", () => this.registeredRecords);
this.registeredRecords = [];
this.registeredEvents.push(server_connections.events().on("notify_handler_deleted", event => {
/* TODO: Remove such pokes (maybe?) ! */
}));
this.modalInstance = spawnModal(
"modal-poked",
[ this.events.generateIpcDescription(), this.variables.generateConsumerDescription() ],
{
popedOut: false,
popoutable: true
}
);
this.modalInstance.getEvents().on("destroy", () => {
if(this.modalInstance) {
this.modalInstance = undefined;
this.destroy();
}
});
this.modalInstance.show().then(undefined);
this.events.on("action_close", () => this.destroy());
}
destroy() {
controllerInstance = undefined;
this.registeredEvents.forEach(callback => callback());
this.registeredEvents = [];
const instance = this.modalInstance;
this.modalInstance = undefined;
instance?.destroy();
this.events.destroy();
this.variables.destroy();
}
registerRecord(record: PokeRecord) {
this.registeredRecords.push(record);
this.variables.sendVariable("pokeList");
this.modalInstance.show().then(undefined);
}
}
export function spawnPokeModal(handler: ConnectionHandler, client: { clientName: string, clientId: number, clientUniqueId: string }, message: string) {
if(!controllerInstance) {
controllerInstance = new PokeController();
}
controllerInstance.registerRecord({
uniqueId: guid(),
handlerId: handler.handlerId,
serverName: handler.channelTree.server.properties.virtualserver_name,
serverUniqueId: handler.channelTree.server.properties.virtualserver_unique_identifier,
clientName: client.clientName,
clientId: client.clientId,
clientUniqueId: client.clientUniqueId,
timestamp: Date.now(),
message: message
});
}

View File

@ -0,0 +1,23 @@
export type PokeRecord = {
uniqueId: string,
timestamp: number,
handlerId: string,
serverName: string,
serverUniqueId: string,
clientId: number,
clientUniqueId: string,
clientName: string,
message: string
}
export interface ModalPokeVariables {
readonly pokeList: PokeRecord[],
}
export interface ModalPokeEvents {
action_close: {}
}

View File

@ -0,0 +1,94 @@
html:root {
--modal-poke-date: cornflowerblue;
--modal-poke-text: #004d00;
}
.container {
display: flex;
flex-direction: column;
padding: 1em;
.containerServers {
display: flex;
flex-direction: column;
justify-content: stretch;
.server {
display: flex;
flex-direction: column;
justify-content: stretch;
.serverName {
flex-grow: 0;
flex-shrink: 0;
font-weight: bold;
text-decoration: underline;
}
&:not(:first-of-type) {
margin-top: 1em;
}
}
}
}
.pokeList {
display: flex;
flex-direction: column;
justify-content: flex-start;
overflow-y: auto;
overflow-x: auto;
.entry {
display: flex;
flex-direction: row;
justify-content: flex-start;
flex-shrink: 0;
flex-grow: 0;
> * {
white-space: nowrap;
}
.date, .user, .text {
margin-right: 5px;
}
.date {
color: var(--modal-poke-date);
}
.text {
color: var(--modal-poke-text);
}
}
}
.buttons {
display: flex;
flex-direction: row;
justify-content: stretch;
margin-top: 1em;
flex-shrink: 0;
flex-grow: 0;
.spacer {
flex-grow: 1;
flex-shrink: 1;
}
.buttonClose {
flex-grow: 0;
flex-shrink: 0;
margin-top: 5px;
width: 150px;
float: right;
}
}

View File

@ -0,0 +1,94 @@
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
import React, {useContext} from "react";
import {Translatable} from "tc-shared/ui/react-elements/i18n";
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
import {ModalPokeEvents, ModalPokeVariables, PokeRecord} from "tc-shared/ui/modal/poke/Definitions";
import {IpcRegistryDescription, Registry} from "tc-events";
import {createIpcUiVariableConsumer, IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable";
import {ClientTag} from "tc-shared/ui/tree/EntryTags";
import {BBCodeRenderer} from "tc-shared/text/bbcode";
import {Button} from "tc-shared/ui/react-elements/Button";
import moment from "moment";
const cssStyle = require("./Renderer.scss");
const VariablesContext = React.createContext<UiVariableConsumer<ModalPokeVariables>>(undefined);
const PokeRenderer = React.memo((props: { poke: PokeRecord }) => (
<div className={cssStyle.entry}>
<div className={cssStyle.date}>{moment(props.poke.timestamp).format("HH:mm:ss")}</div>&nbsp;-&nbsp;
<ClientTag clientName={props.poke.clientName} clientUniqueId={props.poke.clientUniqueId} handlerId={props.poke.handlerId} className={cssStyle.user} />
<div className={cssStyle.text}><Translatable>pokes you</Translatable></div>:
<div><BBCodeRenderer message={props.poke.message} settings={{ convertSingleUrls: true }} /></div>
</div>
))
const ServerPokeListRenderer = React.memo((props: { pokes: PokeRecord[] }) => (
<div className={cssStyle.server}>
<div className={cssStyle.serverName}>{props.pokes.last().serverName}</div>
<div className={cssStyle.pokeList}>
{props.pokes.map(entry => (
<PokeRenderer poke={entry} key={entry.uniqueId} />
))}
</div>
</div>
));
const PokeListRenderer = React.memo(() => {
const variables = useContext(VariablesContext);
const pokes = variables.useReadOnly("pokeList", undefined, []);
let serverPokes: {[key: string]: PokeRecord[]} = {};
pokes.forEach(entry => (serverPokes[entry.serverUniqueId] || (serverPokes[entry.serverUniqueId] = [])).push(entry));
for(const uniqueId of Object.keys(serverPokes)) {
serverPokes[uniqueId].sort((a, b) => a.timestamp - b.timestamp);
}
const sortedServerUniqueIds = Object.keys(serverPokes).sort((a, b) => serverPokes[a][0].timestamp - serverPokes[b][0].timestamp);
return (
<div className={cssStyle.containerServers}>
{sortedServerUniqueIds.map(serverUniqueId => (
<ServerPokeListRenderer pokes={serverPokes[serverUniqueId]} key={serverUniqueId} />
))}
</div>
)
});
class PokeModal extends AbstractModal {
readonly variables: UiVariableConsumer<ModalPokeVariables>;
readonly events: Registry<ModalPokeEvents>;
constructor(events: IpcRegistryDescription<ModalPokeEvents>, variables: IpcVariableDescriptor<ModalPokeVariables>) {
super();
this.variables = createIpcUiVariableConsumer(variables);
this.events = Registry.fromIpcDescription(events);
}
renderBody(): React.ReactElement {
return (
<VariablesContext.Provider value={this.variables}>
<div className={cssStyle.container}>
<PokeListRenderer />
<div className={cssStyle.buttons}>
<div className={cssStyle.spacer} />
<Button color={"green"} onClick={() => this.events.fire("action_close")}>
<Translatable>Close</Translatable>
</Button>
</div>
</div>
</VariablesContext.Provider>
);
}
renderTitle(): string | React.ReactElement {
return <Translatable>You have been poked!</Translatable>;
}
verticalAlignment(): "top" | "center" | "bottom" {
return "top";
}
}
export default PokeModal;

View File

@ -11,6 +11,7 @@ import {
ModalBookmarksAddServerEvents,
ModalBookmarksAddServerVariables
} from "tc-shared/ui/modal/bookmarks-add-server/Definitions";
import {ModalPokeEvents, ModalPokeVariables} from "tc-shared/ui/modal/poke/Definitions";
export type ModalType = "error" | "warning" | "info" | "none";
export type ModalRenderType = "page" | "dialog";
@ -177,5 +178,9 @@ export interface ModalConstructorArguments {
"modal-bookmark-add-server": [
/* events */ IpcRegistryDescription<ModalBookmarksAddServerEvents>,
/* variables */ IpcVariableDescriptor<ModalBookmarksAddServerVariables>,
]
],
"modal-poked": [
/* events */ IpcRegistryDescription<ModalPokeEvents>,
/* variables */ IpcVariableDescriptor<ModalPokeVariables>,
],
}

View File

@ -85,3 +85,10 @@ registerModal({
popoutSupported: true
});
registerModal({
modalId: "modal-poked",
classLoader: async () => await import("tc-shared/ui/modal/poke/Renderer"),
/* FIXME: Make popoutable after the bbcode renderer part has been made popoutable (currently not because of the youtube renderer) */
popoutSupported: false
});

View File

@ -108,11 +108,12 @@ export class InternalModalInstance implements ModalInstanceController {
}
async show() : Promise<void> {
if(!this.modalInstance) {
this.modalInitializePromise = this.constructModal();
if(this.modalInitializePromise) {
await this.modalInitializePromise;
}
if(this.modalInitializePromise) {
if(!this.modalInstance) {
this.modalInitializePromise = this.constructModal();
await this.modalInitializePromise;
}