467 lines
		
	
	
		
			No EOL
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			467 lines
		
	
	
		
			No EOL
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { post } from './utils';
 | 
						|
import CONFIG from './config';
 | 
						|
import Vue from 'vue';
 | 
						|
 | 
						|
import Suggestions from './Suggestions.vue';
 | 
						|
import MessageV from './Message.vue';
 | 
						|
import { Suggestion } from './Suggestions';
 | 
						|
 | 
						|
export interface Message {
 | 
						|
  args: string[];
 | 
						|
  template: string;
 | 
						|
  params?: { [key: string]: string };
 | 
						|
  multiline?: boolean;
 | 
						|
  color?: [ number, number, number ];
 | 
						|
  templateId?: number;
 | 
						|
  mode?: string;
 | 
						|
  modeData?: Mode;
 | 
						|
 | 
						|
  id?: string;
 | 
						|
}
 | 
						|
 | 
						|
export interface ThemeData {
 | 
						|
  style: string;
 | 
						|
  styleSheet: string;
 | 
						|
  baseUrl: string;
 | 
						|
  script: string;
 | 
						|
  templates: { [id: string]: string }; // not supported rn
 | 
						|
  msgTemplates: { [id: string]: string };
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
export interface Mode {
 | 
						|
  name: string;
 | 
						|
  displayName: string;
 | 
						|
  color: string;
 | 
						|
  hidden?: boolean;
 | 
						|
  isChannel?: boolean;
 | 
						|
  isGlobal?: boolean;
 | 
						|
}
 | 
						|
 | 
						|
enum ChatHideStates {
 | 
						|
  ShowWhenActive = 0,
 | 
						|
  AlwaysShow = 1,
 | 
						|
  AlwaysHide = 2,
 | 
						|
}
 | 
						|
 | 
						|
const defaultMode: Mode = {
 | 
						|
  name: 'all',
 | 
						|
  displayName: 'All',
 | 
						|
  color: '#fff'
 | 
						|
};
 | 
						|
 | 
						|
const globalMode: Mode = {
 | 
						|
  name: '_global',
 | 
						|
  displayName: 'All',
 | 
						|
  color: '#fff',
 | 
						|
  isGlobal: true,
 | 
						|
  hidden: true
 | 
						|
};
 | 
						|
 | 
						|
export default Vue.extend({
 | 
						|
  template: "#app_template",
 | 
						|
  name: "app",
 | 
						|
  components: {
 | 
						|
    Suggestions,
 | 
						|
    MessageV
 | 
						|
  },
 | 
						|
  data() {
 | 
						|
    return {
 | 
						|
      style: CONFIG.style,
 | 
						|
      showInput: false,
 | 
						|
      showWindow: false,
 | 
						|
      showHideState: false,
 | 
						|
      hideState: ChatHideStates.ShowWhenActive,
 | 
						|
      backingSuggestions: [] as Suggestion[],
 | 
						|
      removedSuggestions: [] as string[],
 | 
						|
      templates: { ...CONFIG.templates } as { [ key: string ]: string },
 | 
						|
      message: "",
 | 
						|
      messages: [] as Message[],
 | 
						|
      oldMessages: [] as string[],
 | 
						|
      oldMessagesIndex: -1,
 | 
						|
      tplBackups: [] as unknown as [ HTMLElement, string ][],
 | 
						|
      msgTplBackups: [] as unknown as [ string, string ][],
 | 
						|
      focusTimer: 0,
 | 
						|
      showWindowTimer: 0,
 | 
						|
      showHideStateTimer: 0,
 | 
						|
      listener: (event: MessageEvent) => {},
 | 
						|
      modes: [defaultMode, globalMode] as Mode[],
 | 
						|
      modeIdx: 0,
 | 
						|
    };
 | 
						|
  },
 | 
						|
  destroyed() {
 | 
						|
    clearInterval(this.focusTimer);
 | 
						|
    window.removeEventListener("message", this.listener);
 | 
						|
  },
 | 
						|
  mounted() {
 | 
						|
    post("http://chat/loaded", JSON.stringify({}));
 | 
						|
 | 
						|
    this.listener = (event: MessageEvent) => {
 | 
						|
      const item: any = event.data || (<any>event).detail; //'detail' is for debugging via browsers
 | 
						|
 | 
						|
      if (!item || !item.type) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      const typeRef = item.type as
 | 
						|
        'ON_OPEN' | 'ON_SCREEN_STATE_CHANGE' | 'ON_MESSAGE' | 'ON_CLEAR' | 'ON_SUGGESTION_ADD' |
 | 
						|
        'ON_SUGGESTION_REMOVE' | 'ON_TEMPLATE_ADD' | 'ON_UPDATE_THEMES' | 'ON_MODE_ADD' | 'ON_MODE_REMOVE';
 | 
						|
 | 
						|
      if (this[typeRef]) {
 | 
						|
        this[typeRef](item);
 | 
						|
      }
 | 
						|
    };
 | 
						|
 | 
						|
    window.addEventListener("message", this.listener);
 | 
						|
  },
 | 
						|
  watch: {
 | 
						|
    messages() {
 | 
						|
      if (this.hideState !== ChatHideStates.AlwaysHide) {
 | 
						|
        if (this.showWindowTimer) {
 | 
						|
          clearTimeout(this.showWindowTimer);
 | 
						|
        }
 | 
						|
        this.showWindow = true;
 | 
						|
        this.resetShowWindowTimer();
 | 
						|
      }
 | 
						|
 | 
						|
      const messagesObj = this.$refs.messages as HTMLDivElement;
 | 
						|
      this.$nextTick(() => {
 | 
						|
        messagesObj.scrollTop = messagesObj.scrollHeight;
 | 
						|
      });
 | 
						|
    }
 | 
						|
  },
 | 
						|
  computed: {
 | 
						|
    filteredMessages(): Message[] {
 | 
						|
      return this.messages.filter(
 | 
						|
        // show messages that are
 | 
						|
        // - (if the current mode is a channel) global, or in the current mode
 | 
						|
        // - (if the message is a channel) in the current mode
 | 
						|
        el => (el.modeData?.isChannel || this.modes[this.modeIdx].isChannel) ?
 | 
						|
          (el.mode === this.modes[this.modeIdx].name || el.modeData?.isGlobal) :
 | 
						|
          true
 | 
						|
      );
 | 
						|
    },
 | 
						|
 | 
						|
    suggestions(): Suggestion[] {
 | 
						|
      return this.backingSuggestions.filter(
 | 
						|
        el => this.removedSuggestions.indexOf(el.name) <= -1
 | 
						|
      );
 | 
						|
    },
 | 
						|
 | 
						|
    hideAnimated(): boolean {
 | 
						|
      return this.hideState !== ChatHideStates.AlwaysHide;
 | 
						|
    },
 | 
						|
 | 
						|
    modeIdxGet(): number {
 | 
						|
      return (this.modeIdx >= this.modes.length) ? (this.modes.length - 1) : this.modeIdx;
 | 
						|
    },
 | 
						|
 | 
						|
    modePrefix(): string {
 | 
						|
      if (this.modes.length === 2) {
 | 
						|
        return `➤`;
 | 
						|
      }
 | 
						|
 | 
						|
      return this.modes[this.modeIdxGet].displayName;
 | 
						|
    },
 | 
						|
 | 
						|
    modeColor(): string {
 | 
						|
      return this.modes[this.modeIdxGet].color;
 | 
						|
    },
 | 
						|
 | 
						|
    hideStateString(): string {
 | 
						|
      // TODO: localization
 | 
						|
      switch (this.hideState) {
 | 
						|
        case ChatHideStates.AlwaysShow:
 | 
						|
          return 'Visible';
 | 
						|
        case ChatHideStates.AlwaysHide:
 | 
						|
          return 'Hidden';
 | 
						|
        case ChatHideStates.ShowWhenActive:
 | 
						|
          return 'When active';
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
  methods: {
 | 
						|
    ON_SCREEN_STATE_CHANGE({ hideState, fromUserInteraction }: { hideState: ChatHideStates, fromUserInteraction: boolean }) {
 | 
						|
      this.hideState = hideState;
 | 
						|
 | 
						|
      if (this.hideState === ChatHideStates.AlwaysHide) {
 | 
						|
        if (!this.showInput) {
 | 
						|
          this.showWindow = false;
 | 
						|
        }
 | 
						|
      } else if (this.hideState === ChatHideStates.AlwaysShow) {
 | 
						|
        this.showWindow = true;
 | 
						|
        if (this.showWindowTimer) {
 | 
						|
          clearTimeout(this.showWindowTimer);
 | 
						|
        }
 | 
						|
      } else {
 | 
						|
        this.resetShowWindowTimer();
 | 
						|
      }
 | 
						|
 | 
						|
      if (fromUserInteraction) {
 | 
						|
        this.showHideState = true;
 | 
						|
 | 
						|
        if (this.showHideStateTimer) {
 | 
						|
          clearTimeout(this.showHideStateTimer);
 | 
						|
        }
 | 
						|
 | 
						|
        this.showHideStateTimer = window.setTimeout(() => {
 | 
						|
          this.showHideState = false;
 | 
						|
        }, 1500);
 | 
						|
      }
 | 
						|
    },
 | 
						|
    ON_OPEN() {
 | 
						|
      this.showInput = true;
 | 
						|
      this.showWindow = true;
 | 
						|
      if (this.showWindowTimer) {
 | 
						|
        clearTimeout(this.showWindowTimer);
 | 
						|
      }
 | 
						|
      this.focusTimer = window.setInterval(() => {
 | 
						|
        if (this.$refs.input) {
 | 
						|
          (this.$refs.input as HTMLInputElement).focus();
 | 
						|
        } else {
 | 
						|
          clearInterval(this.focusTimer);
 | 
						|
        }
 | 
						|
      }, 100);
 | 
						|
    },
 | 
						|
    ON_MESSAGE({ message }: { message: Message }) {
 | 
						|
      message.id = `${new Date().getTime()}${Math.random()}`;
 | 
						|
      message.modeData = this.modes.find(mode => mode.name === message.mode);
 | 
						|
      this.messages.push(message);
 | 
						|
    },
 | 
						|
    ON_CLEAR() {
 | 
						|
      this.messages = [];
 | 
						|
      this.oldMessages = [];
 | 
						|
      this.oldMessagesIndex = -1;
 | 
						|
    },
 | 
						|
    ON_SUGGESTION_ADD({ suggestion }: { suggestion: Suggestion }) {
 | 
						|
      this.removedSuggestions = this.removedSuggestions.filter(a => a !== suggestion.name);
 | 
						|
      const duplicateSuggestion = this.backingSuggestions.find(
 | 
						|
        a => a.name == suggestion.name
 | 
						|
      );
 | 
						|
      if (duplicateSuggestion) {
 | 
						|
        if (suggestion.help || suggestion.params) {
 | 
						|
          duplicateSuggestion.help = suggestion.help || "";
 | 
						|
          duplicateSuggestion.params = suggestion.params || [];
 | 
						|
        }
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      if (!suggestion.params) {
 | 
						|
        suggestion.params = []; //TODO Move somewhere else
 | 
						|
      }
 | 
						|
      this.backingSuggestions.push(suggestion);
 | 
						|
    },
 | 
						|
    ON_SUGGESTION_REMOVE({ name }: { name: string }) {
 | 
						|
      if (this.removedSuggestions.indexOf(name) <= -1) {
 | 
						|
        this.removedSuggestions.push(name);
 | 
						|
      }
 | 
						|
    },
 | 
						|
    ON_MODE_ADD({ mode }: { mode: Mode }) {
 | 
						|
      this.modes = [
 | 
						|
        ...this.modes.filter(a => a.name !== mode.name),
 | 
						|
        mode
 | 
						|
      ];
 | 
						|
    },
 | 
						|
    ON_MODE_REMOVE({ name }: { name: string }) {
 | 
						|
      this.modes = this.modes.filter(a => a.name !== name);
 | 
						|
 | 
						|
      if (this.modes.length === 0) {
 | 
						|
        this.modes = [defaultMode];
 | 
						|
      }
 | 
						|
    },
 | 
						|
    ON_TEMPLATE_ADD({ template }: { template: { id: string, html: string }}) {
 | 
						|
      if (this.templates[template.id]) {
 | 
						|
        this.warn(`Tried to add duplicate template '${template.id}'`);
 | 
						|
      } else {
 | 
						|
        this.templates[template.id] = template.html;
 | 
						|
      }
 | 
						|
    },
 | 
						|
    ON_UPDATE_THEMES({ themes }: { themes: { [key: string]: ThemeData } }) {
 | 
						|
      this.removeThemes();
 | 
						|
 | 
						|
      this.setThemes(themes);
 | 
						|
    },
 | 
						|
    removeThemes() {
 | 
						|
      for (let i = 0; i < document.styleSheets.length; i++) {
 | 
						|
        const styleSheet = document.styleSheets[i];
 | 
						|
        const node = styleSheet.ownerNode as Element;
 | 
						|
 | 
						|
        if (node.getAttribute("data-theme")) {
 | 
						|
          node.parentNode?.removeChild(node);
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      this.tplBackups.reverse();
 | 
						|
 | 
						|
      for (const [elem, oldData] of this.tplBackups) {
 | 
						|
        elem.innerText = oldData;
 | 
						|
      }
 | 
						|
 | 
						|
      this.tplBackups = [];
 | 
						|
 | 
						|
      this.msgTplBackups.reverse();
 | 
						|
 | 
						|
      for (const [id, oldData] of this.msgTplBackups) {
 | 
						|
        this.templates[id] = oldData;
 | 
						|
      }
 | 
						|
 | 
						|
      this.msgTplBackups = [];
 | 
						|
    },
 | 
						|
    setThemes(themes: { [key: string]: ThemeData }) {
 | 
						|
      for (const [id, data] of Object.entries(themes)) {
 | 
						|
        if (data.style) {
 | 
						|
          const style = document.createElement("style");
 | 
						|
          style.type = "text/css";
 | 
						|
          style.setAttribute("data-theme", id);
 | 
						|
          style.appendChild(document.createTextNode(data.style));
 | 
						|
 | 
						|
          document.head.appendChild(style);
 | 
						|
        }
 | 
						|
 | 
						|
        if (data.styleSheet) {
 | 
						|
          const link = document.createElement("link");
 | 
						|
          link.rel = "stylesheet";
 | 
						|
          link.type = "text/css";
 | 
						|
          link.href = data.baseUrl + data.styleSheet;
 | 
						|
          link.setAttribute("data-theme", id);
 | 
						|
 | 
						|
          document.head.appendChild(link);
 | 
						|
        }
 | 
						|
 | 
						|
        if (data.templates) {
 | 
						|
          for (const [tplId, tpl] of Object.entries(data.templates)) {
 | 
						|
            const elem = document.getElementById(tplId);
 | 
						|
 | 
						|
            if (elem) {
 | 
						|
              this.tplBackups.push([elem, elem.innerText]);
 | 
						|
              elem.innerText = tpl;
 | 
						|
            }
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        if (data.script) {
 | 
						|
          const script = document.createElement("script");
 | 
						|
          script.type = "text/javascript";
 | 
						|
          script.src = data.baseUrl + data.script;
 | 
						|
 | 
						|
          document.head.appendChild(script);
 | 
						|
        }
 | 
						|
 | 
						|
        if (data.msgTemplates) {
 | 
						|
          for (const [tplId, tpl] of Object.entries(data.msgTemplates)) {
 | 
						|
            this.msgTplBackups.push([tplId, this.templates[tplId]]);
 | 
						|
            this.templates[tplId] = tpl;
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
    },
 | 
						|
    warn(msg: string) {
 | 
						|
      this.messages.push({
 | 
						|
        args: [msg],
 | 
						|
        template: "^3<b>CHAT-WARN</b>: ^0{0}"
 | 
						|
      });
 | 
						|
    },
 | 
						|
    clearShowWindowTimer() {
 | 
						|
      clearTimeout(this.showWindowTimer);
 | 
						|
    },
 | 
						|
    resetShowWindowTimer() {
 | 
						|
      this.clearShowWindowTimer();
 | 
						|
      this.showWindowTimer = window.setTimeout(() => {
 | 
						|
        if (this.hideState !== ChatHideStates.AlwaysShow && !this.showInput) {
 | 
						|
          this.showWindow = false;
 | 
						|
        }
 | 
						|
      }, CONFIG.fadeTimeout);
 | 
						|
    },
 | 
						|
    keyUp() {
 | 
						|
      this.resize();
 | 
						|
    },
 | 
						|
    keyDown(e: KeyboardEvent) {
 | 
						|
      if (e.which === 38 || e.which === 40) {
 | 
						|
        e.preventDefault();
 | 
						|
        this.moveOldMessageIndex(e.which === 38);
 | 
						|
      } else if (e.which == 33) {
 | 
						|
        var buf = document.getElementsByClassName("chat-messages")[0];
 | 
						|
        buf.scrollTop = buf.scrollTop - 100;
 | 
						|
      } else if (e.which == 34) {
 | 
						|
        var buf = document.getElementsByClassName("chat-messages")[0];
 | 
						|
        buf.scrollTop = buf.scrollTop + 100;
 | 
						|
      } else if (e.which === 9) { // tab
 | 
						|
        if (e.shiftKey || e.altKey) {
 | 
						|
          do {
 | 
						|
            --this.modeIdx;
 | 
						|
 | 
						|
            if (this.modeIdx < 0) {
 | 
						|
              this.modeIdx = this.modes.length - 1;
 | 
						|
            }
 | 
						|
          } while (this.modes[this.modeIdx].hidden);
 | 
						|
        } else {
 | 
						|
          do {
 | 
						|
            this.modeIdx = (this.modeIdx + 1) % this.modes.length;
 | 
						|
          } while (this.modes[this.modeIdx].hidden);
 | 
						|
        }
 | 
						|
 | 
						|
        const buf = document.getElementsByClassName('chat-messages')[0];
 | 
						|
        setTimeout(() => buf.scrollTop = buf.scrollHeight, 0);
 | 
						|
      }
 | 
						|
 | 
						|
      this.resize();
 | 
						|
    },
 | 
						|
    moveOldMessageIndex(up: boolean) {
 | 
						|
      if (up && this.oldMessages.length > this.oldMessagesIndex + 1) {
 | 
						|
        this.oldMessagesIndex += 1;
 | 
						|
        this.message = this.oldMessages[this.oldMessagesIndex];
 | 
						|
      } else if (!up && this.oldMessagesIndex - 1 >= 0) {
 | 
						|
        this.oldMessagesIndex -= 1;
 | 
						|
        this.message = this.oldMessages[this.oldMessagesIndex];
 | 
						|
      } else if (!up && this.oldMessagesIndex - 1 === -1) {
 | 
						|
        this.oldMessagesIndex = -1;
 | 
						|
        this.message = "";
 | 
						|
      }
 | 
						|
    },
 | 
						|
    resize() {
 | 
						|
      const input = this.$refs.input as HTMLInputElement;
 | 
						|
 | 
						|
      // scrollHeight includes padding, but content-box excludes padding
 | 
						|
      // remove padding before setting height on the element
 | 
						|
      const style = getComputedStyle(input);
 | 
						|
      const paddingRemove = parseFloat(style.paddingBottom) + parseFloat(style.paddingTop);
 | 
						|
 | 
						|
      input.style.height = "5px";
 | 
						|
      input.style.height = `${input.scrollHeight - paddingRemove}px`;
 | 
						|
    },
 | 
						|
    send() {
 | 
						|
      if (this.message !== "") {
 | 
						|
        post(
 | 
						|
          "http://chat/chatResult",
 | 
						|
          JSON.stringify({
 | 
						|
            message: this.message,
 | 
						|
            mode: this.modes[this.modeIdxGet].name
 | 
						|
          })
 | 
						|
        );
 | 
						|
        this.oldMessages.unshift(this.message);
 | 
						|
        this.oldMessagesIndex = -1;
 | 
						|
        this.hideInput();
 | 
						|
      } else {
 | 
						|
        this.hideInput(true);
 | 
						|
      }
 | 
						|
    },
 | 
						|
    hideInput(canceled = false) {
 | 
						|
      setTimeout(() => {
 | 
						|
        const input = this.$refs.input as HTMLInputElement;
 | 
						|
        delete input.style.height;
 | 
						|
      }, 50);
 | 
						|
 | 
						|
      if (canceled) {
 | 
						|
        post("http://chat/chatResult", JSON.stringify({ canceled }));
 | 
						|
      }
 | 
						|
      this.message = "";
 | 
						|
      this.showInput = false;
 | 
						|
      clearInterval(this.focusTimer);
 | 
						|
 | 
						|
      if (this.hideState !== ChatHideStates.AlwaysHide) {
 | 
						|
        this.resetShowWindowTimer();
 | 
						|
      } else {
 | 
						|
        this.showWindow = false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
}); |