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-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"
|
||||
|
|
|
@ -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>
|
||||
</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>
|
||||
|
|
|
@ -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", {
|
||||
|
|
|
@ -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]";
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
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>,
|
||||
],
|
||||
}
|
|
@ -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
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue