Using a react poke modal
parent
081ed060ed
commit
89b63a425d
|
@ -18,7 +18,6 @@ import "./static/modal-identity.scss"
|
||||||
import "./static/modal-newcomer.scss"
|
import "./static/modal-newcomer.scss"
|
||||||
import "./static/modal-invite.scss"
|
import "./static/modal-invite.scss"
|
||||||
import "./static/modal-keyselect.scss"
|
import "./static/modal-keyselect.scss"
|
||||||
import "./static/modal-poke.scss"
|
|
||||||
import "./static/modal-query.scss"
|
import "./static/modal-query.scss"
|
||||||
import "./static/modal-server.scss"
|
import "./static/modal-server.scss"
|
||||||
import "./static/modal-musicmanage.scss"
|
import "./static/modal-musicmanage.scss"
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1179,16 +1179,6 @@
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</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">
|
<script class="jsrender-template" id="tmpl_query_create" type="text/html">
|
||||||
<div class="query-create">
|
<div class="query-create">
|
||||||
<a>{{tr "Set the login name for your Server Query account." /}}</a>
|
<a>{{tr "Set the login name for your Server Query account." /}}</a>
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {ErrorCode} from "../connection/ErrorCode";
|
||||||
import {server_connections} from "tc-shared/ConnectionManager";
|
import {server_connections} from "tc-shared/ConnectionManager";
|
||||||
import {ChannelEntry} from "tc-shared/tree/Channel";
|
import {ChannelEntry} from "tc-shared/tree/Channel";
|
||||||
import {EventClient} from "tc-shared/connectionlog/Definitions";
|
import {EventClient} from "tc-shared/connectionlog/Definitions";
|
||||||
|
import {spawnPokeModal} from "tc-shared/ui/modal/poke/Controller";
|
||||||
|
|
||||||
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
|
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
|
||||||
constructor(connection: AbstractServerConnection) {
|
constructor(connection: AbstractServerConnection) {
|
||||||
|
@ -982,10 +983,11 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
||||||
|
|
||||||
handleNotifyClientPoke(json) {
|
handleNotifyClientPoke(json) {
|
||||||
json = json[0];
|
json = json[0];
|
||||||
spawnPoke(this.connection_handler, {
|
|
||||||
id: parseInt(json["invokerid"]),
|
spawnPokeModal(this.connection_handler, {
|
||||||
name: json["invokername"],
|
clientId: parseInt(json["invokerid"]),
|
||||||
unique_id: json["invokeruid"]
|
clientName: json["invokername"],
|
||||||
|
clientUniqueId: json["invokeruid"]
|
||||||
}, json["msg"]);
|
}, json["msg"]);
|
||||||
|
|
||||||
this.connection_handler.log.log("client.poke.received", {
|
this.connection_handler.log.log("client.poke.received", {
|
||||||
|
|
|
@ -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]";
|
return "[url=channel://" + this.channelId + "/" + encodeURIComponent(this.properties.channel_name) + "]" + this.formattedChannelName() + "[/url]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
});
|
||||||
|
}
|
|
@ -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: {}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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> -
|
||||||
|
<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;
|
|
@ -11,6 +11,7 @@ import {
|
||||||
ModalBookmarksAddServerEvents,
|
ModalBookmarksAddServerEvents,
|
||||||
ModalBookmarksAddServerVariables
|
ModalBookmarksAddServerVariables
|
||||||
} from "tc-shared/ui/modal/bookmarks-add-server/Definitions";
|
} 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 ModalType = "error" | "warning" | "info" | "none";
|
||||||
export type ModalRenderType = "page" | "dialog";
|
export type ModalRenderType = "page" | "dialog";
|
||||||
|
@ -177,5 +178,9 @@ export interface ModalConstructorArguments {
|
||||||
"modal-bookmark-add-server": [
|
"modal-bookmark-add-server": [
|
||||||
/* events */ IpcRegistryDescription<ModalBookmarksAddServerEvents>,
|
/* events */ IpcRegistryDescription<ModalBookmarksAddServerEvents>,
|
||||||
/* variables */ IpcVariableDescriptor<ModalBookmarksAddServerVariables>,
|
/* variables */ IpcVariableDescriptor<ModalBookmarksAddServerVariables>,
|
||||||
]
|
],
|
||||||
|
"modal-poked": [
|
||||||
|
/* events */ IpcRegistryDescription<ModalPokeEvents>,
|
||||||
|
/* variables */ IpcVariableDescriptor<ModalPokeVariables>,
|
||||||
|
],
|
||||||
}
|
}
|
|
@ -85,3 +85,10 @@ registerModal({
|
||||||
popoutSupported: true
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
@ -108,11 +108,12 @@ export class InternalModalInstance implements ModalInstanceController {
|
||||||
}
|
}
|
||||||
|
|
||||||
async show() : Promise<void> {
|
async show() : Promise<void> {
|
||||||
if(!this.modalInstance) {
|
if(this.modalInitializePromise) {
|
||||||
this.modalInitializePromise = this.constructModal();
|
await this.modalInitializePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.modalInitializePromise) {
|
if(!this.modalInstance) {
|
||||||
|
this.modalInitializePromise = this.constructModal();
|
||||||
await this.modalInitializePromise;
|
await this.modalInitializePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue