You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
RnQ/Distro/Template/chat.htm

3900 lines
120 KiB
HTML

<html>
<head>
<include src="common.htm" />
<style src="chat.css" />
<style src="colorizer.css" />
<style id="dyncss">
.input > text::mark(wrongword) {
text-decoration: underline dotted #AB4145;
}
</style>
<style id="usercss"></style>
<script src="hint.js" />
<script src="menus/contactmenu.js" />
<script>
const Native = View.share.ChatNative;
const icqfilelnk = "https://files.icq.net/get/";
// include "ddm.tis";
var isActive = false,
initialLoad = true,
msgPreview = false,
lastSmartReplies = {},
rightClickedTime = "0",
rightClickedData = null,
scrollbarActive = false,
draggingTab = false,
draggingTarget = null,
skipAltUp = false,
skipMBtnUp = false,
embeddedCnt = 0,
sMgr = null,
sKeywords = "",
settings = {
showSmiles: true,
showRelTimes: false,
showAvatar: true,
showSmileCaption: false,
showHintsInChat: true,
showSmartReplies: false,
showReactions: true,
autoCopy: true,
smoothFontRendering: true,
fontCodes: false,
viewTextWrap: true,
animatedScroll: true,
quoteSelected: true,
cursorBelow: true,
alwaysOnTop: false,
wheelVelocity: 3,
imageQuality: 0,
maxImgWidth: 0,
maxImgHeight: 0,
msgBuffer: 20,
sendOnEnter: 0,
screenshotFormat: 0,
screenshotFilename: "snapshot.png",
spellErrorStyle: 0,
spellErrorColor: "",
cachePath: "",
chatCSS: ""
},
pages = $("#pages"),
tabs = $("#tabs"),
backTabs = $("#backTabs"),
sendBtn = $("#sendBtn"),
sendBtnImg = $("#sendBtn > div"),
sendVariant = $("#sendVariant"),
closeBtn = $("#closeBtn"),
closeVariant = $("#closeVariant"),
sendmenu = $("#sendmenu"),
closemenu = $("#closemenu"),
filemenu = $("#filemenu"),
snapmenu = $("#snapmenu"),
subscreens = $("#subscreens"),
codemenu = $("#codemenu"),
monIndicator = $("#monIndicator"),
plugButtons = $("#plugButtons"),
histmenu = {},
chatButtons = {},
dragFile = $("#dragFile"),
dragHtml = $("#dragHtml"),
patches = $("#patches"),
myreactions = $("#myreactions"),
linkinfo = $("#linkinfo"),
overlay, snapLayer, videoview,
closePreview = (e = null) => {
overlay.off("wheel");
overlay.off("^mousedown");
overlay.on("transitionend", () => {
if (overlay == null) return;
overlay.remove();
overlay = null;
});
overlay.classList.remove("active");
return true;
},
updateTabsDepth = () => {
for (let tab of tabs.children) tab.updateDepth();
//tabs.update();
};
class ControlledList extends Element {
componentDidMount(byCSS) {
this.state.focusable = true;
this.initList();
}
initList(focus = false) {
this.list = this.children[0];
if (!this.list) return;
if (this.list.length > 0) {
var cur = this.list.$(":current");
if (cur) this.scrollToViewWithSpacing(cur); else {
this.list.children[0].state.current = true;
this.scrollTo({
position: [0, 0],
behavior: "instant"
});
}
}
if (focus) this.list.state.focus = true;
}
scrollToViewWithSpacing(el) {
if ((el.state.box("top", "margin", "parent") == 0 && this.scrollTop == 0) ||
(el.state.box("bottom", "margin", "parent") == this.list.state.box("height") && this.scrollBottom == 0)) return;
el.scrollIntoView({
behavior: "instant",
block: "nearest"
});
this.flushPaint();
var pBottom = this.list.style["padding-bottom"].valueOf();
var pTop = this.list.style["padding-top"].valueOf();
var sTop = this.scrollTop;
var elBottom = el.state.box("bottom", "border", "parent");
if (elBottom + pBottom + pTop > sTop + this.state.box("height", "padding")) {
this.scrollTo({
position: [0, sTop + pTop],
behavior: "instant"
});
}
var elTop = el.state.box("top", "border", "parent");
if (elTop < sTop)
this.scrollTo({
position: [0, elTop],
behavior: "instant"
});
}
["on keydown"](e) {
if (this.list.length == 0) return;
var c = this.list.$(":current"), n = null, imgCat = null;
if (!c) {
this.list.first.state.current = true;
return true;
}
var inarow = 7;
var pgstep = 5;
if (c.$p("#smilesList")) {
inarow = settings.showSmileCaption ? 6 : 14;
} else if (c.$p("#emojiList")) {
inarow = 8;
pgstep = 4;
} else if (c.$p("#stickersList")) {
inarow = 4;
pgstep = 2;
}
switch (e.keyCode) {
case Event.VK_RIGHT:
n = c.next ? (c.$p("#smilesList") ? c.next.next : c.next) : null;
if (!n) return true;
c.state.current = false;
n.state.current = true;
this.scrollToViewWithSpacing(n);
return true;
break;
case Event.VK_LEFT:
n = c.prior ? (c.$p("#smilesList") ? c.prior.prior : c.prior) : null;
if (!n) return true;
c.state.current = false;
n.state.current = true;
this.scrollToViewWithSpacing(n);
return true;
break;
case Event.VK_DOWN:
case Event.VK_NEXT:
n = c.parent.children[c.index + inarow * (e.keyCode == Event.VK_NEXT ? pgstep : 1)];
let lstrw = c.parent.length % inarow;
if (!n) n = c.parent.children[c.parent.length - (lstrw == 0 ? inarow : lstrw) + c.index % inarow];
//if (!n) n = c.parent.children[c.parent.length - c.parent.length % inarow - inarow + c.index % inarow];
if (!n) n = c.$p("#smilesList") ? this.list.last.prior : this.list.last;
c.state.current = false;
n.state.current = true;
this.scrollToViewWithSpacing(n);
return true;
break;
case Event.VK_UP:
case Event.VK_PRIOR:
n = c.parent.children[c.index - inarow * (e.keyCode == Event.VK_PRIOR ? pgstep : 1)];
if (!n) n = c.parent.children[c.index % inarow];
c.state.current = false;
n.state.current = true;
this.scrollToViewWithSpacing(n);
return true;
break;
case Event.VK_END:
n = c.$p("#smilesList") ? this.list.last.prior : this.list.last;
c.state.current = false;
n.state.current = true;
this.scrollToViewWithSpacing(n);
return true;
break;
case Event.VK_HOME:
n = this.list.first;
c.state.current = false;
n.state.current = true;
this.scrollToViewWithSpacing(n);
return true;
break;
case Event.VK_RETURN:
case Event.VK_SPACE:
if (this.$p("#stickersList")) {
var stsearch = $("#stsearch");
if (stsearch && stsearch.state.focus) break;
Native.SendStickerToCurrent(c.attr["sticker"]);
} else {
getCurrentPage().addToInput(c.attr["text"]);
}
this.parent.state.popup = false;
break;
case Event.VK_ESCAPE:
getCurrentPage().input.state.focus = true;
break;
}
}
}
class HistScroller extends Element {
scrollToEx(x, y, animate = true) {
this.scrollTo({
position: [x, y],
behavior: animate ? "smooth" : "instant"
});
this.$p(".page").checkScrollPosition();
}
}
class MakeCurrent extends Element {
componentDidMount() {
var el = this.attr["class"] == "caption" ? this.prior : this;
var e = el.parent.$(":current");
if (e) e.state.current = false;
el.state.current = true;
}
}
class TitleHover extends Element {
reactBtn;
componentDidMount() {
if (!settings.showReactions || msgPreview) return;
this.reactBtn = this.$(".msgReactBtn");
this.reactBtn.setVisible(true);
this.reactBtn.classList.remove("hidden");
}
componentWillUnmount() {
if (!settings.showReactions || !this.reactBtn) return;
myreactions.state.popup = false;
this.reactBtn.classList.add("hidden");
}
}
class CatButton extends Element {
componentDidMount() {
this.on("click", function(e) {
for (let btn of this.parent.children)
btn.state.checked = false;
this.state.checked = true;
});
}
componentWillUnmount() {
this.off("click");
}
}
class ImageGrid extends Element {
onpopupdismissing(e) {
this.post(() => {
if (this === smilesGrid) this.detach();
else if (this === stickersGrid) stickersList.clear();
});
}
["on keydown"](e) {
if (e.keyCode == Event.VK_ESCAPE) {
this.state.popup = false;
return true;
} else if (e.keyCode == Event.VK_TAB) return true;
}
["on ^keydown"](e) {
if (e.keyCode !== Event.VK_TAB) return false;
var imgCat = this.$(".imagecat");
if (!imgCat) return false;
var c = imgCat.$(":checked");
if (!c) return false;
var n = e.shiftKey ? (c.prior ? c.prior : c.parent.last) : (c.next ? c.next : c.parent.first);
if (!n) return false;
for (let btn of imgCat.children)
btn.state.checked = false;
this.state.focus = true;
if (this === stickersGrid) n.scrollIntoView({
behavior: "smooth",
block: "nearest"
});
n.dispatchEvent(new Event("click", { bubbles: true }));
return true;
}
}
class VideoPreview extends Element {
page = null;
videoinit = false;
videotimecode = "00s";
videotitle;
videocodecs;
videoformat;
videolink;
videokind;
vollevel = 0.85;
animCnt = 0;
animTotal = 30.0;
render() {
return <div id="videoview">
<div id="videoplayer">
<div id="playerdisplay">
<video id="video" />
<div id="videoload"></div>
<svg id="play" viewbox="1 1 28 28">
<path d="M 11,8 21,15 11,22 z"/>
</svg>
<svg id="pause" viewbox="1 1 28 28">
<path d="M 8,8 13,8 13,22 8,22 z M 17,8 22,8 22,22 17,22 z"/>
</svg>
</div>
<div id="playercontrols">
<div id="controls">
<input uwp compact type="hslider" id="videopos" />
<output value="00:00 / 00:00" id="videotime" />
<input uwp compact type="hslider" id="videovol" value="50" min="1" max="100" />
<div id="info" titleid="videoinfo">
<svg viewBox="0 0 12.581 25.837">
<path d="M5.109,25.456c-3.041,1.068-5.547-0.157-5.044-3.077c0.503-2.92,3.388-9.172,3.799-10.354 c0.412-1.183-0.377-1.506-1.223-1.025c-0.487,0.281-1.212,0.845-1.834,1.393c-0.172-0.348-0.415-0.744-0.598-1.125 c1.015-1.017,2.712-2.381,4.721-2.875c2.4-0.592,6.411,0.354,4.688,4.942c-1.232,3.269-2.103,5.526-2.65,7.206 C6.419,22.222,7.07,22.574,8.03,21.92c0.75-0.512,1.549-1.208,2.135-1.749c0.271,0.441,0.358,0.581,0.626,1.087 C9.774,22.299,7.115,24.738,5.109,25.456z M11.408,5.229c-1.38,1.174-3.424,1.148-4.566-0.057C5.698,3.966,5.889,2.038,7.267,0.864 c1.379-1.174,3.423-1.148,4.566,0.057C12.977,2.126,12.787,4.054,11.408,5.229z"/>
</svg>
</div>
<div id="upload">
<svg height="1000" width="928.571" xmlns="http://www.w3.org/2000/svg">
<path d="M714.24 821.44q0 -14.508 -10.602 -25.11t-25.11 -10.602 -25.11 10.602 -10.602 25.11 10.602 25.11 25.11 10.602 25.11 -10.602 10.602 -25.11zm142.848 0q0 -14.508 -10.602 -25.11t-25.11 -10.602 -25.11 10.602 -10.602 25.11 10.602 25.11 25.11 10.602 25.11 -10.602 10.602 -25.11zm71.424 -124.992v178.56q0 22.32 -15.624 37.944t-37.944 15.624h-821.376q-22.32 0 -37.944 -15.624t-15.624 -37.944v-178.56q0 -22.32 15.624 -37.944t37.944 -15.624h238.266q11.718 31.248 39.339 51.336t61.659 20.088h142.848q34.038 0 61.659 -20.088t39.339 -51.336h238.266q22.32 0 37.944 15.624t15.624 37.944zm-181.35 -361.584q-9.486 22.32 -32.922 22.32h-142.848v249.984q0 14.508 -10.602 25.11t-25.11 10.602h-142.848q-14.508 0 -25.11 -10.602t-10.602 -25.11v-249.984h-142.848q-23.436 0 -32.922 -22.32 -9.486 -21.762 7.812 -38.502l249.984 -249.984q10.044 -10.602 25.11 -10.602t25.11 10.602l249.984 249.984q17.298 16.74 7.812 38.502z"/>
</svg>
</div>
<div id="close">
<svg viewBox="0 0 480 480">
<path d="M310.182,235.995l103.285-103.259c5.006-5.018,5.006-13.237,0-18.251l-54.721-54.733c-5.014-5-13.229-5-18.24,0 l-103.281,103.28L133.944,59.752c-5.018-5-13.229-5-18.246,0l-54.717,54.733c-5.008,5.014-5.008,13.233,0,18.251l103.281,103.259 L60.999,339.263c-5.018,5.014-5.018,13.232,0,18.25l54.717,54.738c5.018,5.001,13.229,5.001,18.242,0l103.268-103.285 l103.264,103.285c5.018,5.001,13.229,5.001,18.24,0l54.721-54.738c5.014-5.018,5.014-13.236,0-18.25L310.182,235.995z"/>
</svg>
</div>
</div>
</div>
</div>
</div>
}
initChildren() {
this.video = $("#video"),
this.videopos = $("#videopos"),
this.videotime = $("#videotime"),
this.videovol = $("#videovol"),
this.videoload = $("#videoload"),
this.videoinfo = $("#videoinfo"),
this.videoplay = $("#play"),
this.videopause = $("#pause"),
this.videoupload = $("#upload"),
this.videoclose = $("#close"),
this.videocontrols = $("#controls");
this.videoplay.on("click", () => this.playpauseSwitch());
this.videopause.on("click", () => this.playpauseSwitch());
this.video.on("click", () => this.playpauseSwitch());
this.videopos.on("click", (e) => this.play(e.target.value));
this.videovol.on("change", (e) => {
this.vollevel = this.logslider(e.target.value);
this.video.video.audioVolume = this.vollevel;
e.target.timer(1s, () => { Native.SaveVolumeLevel(this.vollevel) });
});
this.videotime.on("mousedown", (e) => {
var lnk;
if (this.kind == "youtube")
lnk = this.videolink + (this.videolink.indexOf("?") >= 0 ? "&t=" : "?t=") + this.videotimecode;
else if (this.kind == "vimeo")
lnk = this.videolink + "#t=" + this.videotimecode;
if (e.mainButton) {
this.closeVideo();
CommonNative.OpenLink(lnk);
} else {
this.page.input.value = lnk;
this.page.changeInput();
}
});
this.video.on("videostart", (e) => this.alignPlayerControls());
this.video.on("videoready", (e) => {
this.videoinit = true;
var content = this.videotitle + "

"
+ _("Resolution:") + " " + this.video.video.width + "x" + this.video.video.height;
if (this.videoformat) content += "
"
+ _("Format:") + " " + this.videoformat;
if (this.videocodecs) content += "
"
+ _("Codecs:") + " " + this.videocodecs;
this.videoinfo.html = content;
this.alignPlayerControls();
});
this.videoclose.on("click", () => this.closeVideo());
this.videoupload.on("click", () => {
if (!settings.cachePath) return;
var image = new Graphics.Image(this.video.video.width, this.video.video.height, this.video);
image.toBytes(settings.screenshotFormatSymbol, 95).save(settings.cachePath + settings.screenshotFilename);
image = null;
Native.UploadLastSnapshot();
});
this.videotime.attr["title"] = _("Get link with current time [LMB - open in browser, RMB - insert to chat input]");
this.videoupload.attr["title"] = _("Make snapshot and upload it to server");
this.videoclose.attr["title"] = _("Close video preview");
}
componentWillUnmount() {
this.videoplay.off("click");
this.videopause.off("click");
this.videopos.off("click");
this.videovol.off("change");
this.videotime.off("mousedown");
this.videoclose.off("click");
this.videoupload.off("click");
this.videoload.off("click");
this.video.off("click");
this.video.off("ready");
this.video.off("start");
}
animFunc(el) {
var opacityVal = this.easeOutCubic(this.animCnt, 0.6, -0.6, this.animTotal);
var scaleVal = this.easeOutCubic(this.animCnt, 1.0, 0.8, this.animTotal);
el.style.set({
opacity: opacityVal,
transform: "scale(" + scaleVal + ") translate(-50dip, -50dip)"
});
this.animCnt++;
if (this.animCnt > this.animTotal) {
el.style.display = "none";
return false;
} else {
el.style.display = "block";
return true;
}
}
playpauseSwitch() {
if (!this.videoinit) return;
this.videoplay.style.display = "none";
this.videopause.style.display = "none";
if (this.video.video.isPlaying) {
this.videoplay.timer(0, () => this.animFunc(this.videoplay));
this.animCnt = 0;
this.videopause.timer(10ms, () => this.animFunc(this.videopause));
this.video.video.stop();
} else {
this.videopause.timer(0, () => this.animFunc(this.videopause));
this.animCnt = 0;
this.videoplay.timer(10ms, () => this.animFunc(this.videoplay));
if (this.video.video.isEnded)
this.video.video.position = 0;
this.video.video.play();
}
}
closeVideo() {
this.video.video.unload();
this.videoinit = false;
this.videocontrols.classList.remove("visible");
this.classList.remove("visible");
}
show() {
this.videocontrols.classList.remove("visible");
this.classList.add("visible");
}
isVisible() {
return this.classList.contains("visible");
}
load(link) {
return this.video.video.load(link);
}
play(pos) {
this.video.video.position = pos;
this.video.video.play();
}
startPulse() {
this.video.timer(500ms, () => this.pulse());
this.pulse();
}
startVideo(url, format, codecs) {
this.setLoadingText(_("Loading video stream..."));
this.videoformat = format;
this.videocodecs = codecs;
if (this.load(url)) {
this.volume = Native.GetVolumeLevel();
this.play(0);
this.startPulse();
} else {
this.error();
}
this.setLoadingText(null);
}
setLoadingText(content, showFormats = false) {
if (content == null)
this.videoload.clear();
else
this.videoload.content(content);
this.videoload.on("click", "a[close]", () => this.closeVideo());
if (showFormats) this.videoload.on("click", "div[format]", (e) => {
this.startVideo(e.target.attr["url"], e.target.attr["format"], e.target.attr["codecs"]);
});
}
set init(val) {
this.videoinit = val;
}
get init() {
return this.videoinit;
}
set title(val) {
this.videotitle = val;
}
get title() {
return this.videotitle;
}
set src(val) {
this.videolink = val;
}
get src() {
return this.videolink;
}
set kind(val) {
this.videokind = val;
}
get kind() {
return this.videokind;
}
set volume(val) {
this.vollevel = val;
this.videovol.value = this.expslider(val);
this.video.video.audioVolume = val;
}
get volume() {
return this.vollevel;
}
error() {
this.videoinit = false;
this.classList.remove("visible");
createDialog(
"hand",
_("Error"),
_("Failed to load video")
);
}
easeOutCubic(currentIteration, startValue, changeInValue, totalIterations) {
return changeInValue * (Math.pow(currentIteration / totalIterations - 1.0, 3.0) + 1.0) + startValue;
}
logslider(pos) {
return Math.log10(pos) / 2;
}
expslider(val) {
return Math.pow(10, val * 2);
}
hms(seconds) {
var h, m, s;
s = seconds % 60; seconds /= 60;
m = seconds % 60; seconds /= 60;
h = seconds % 60; return [h, m, s];
}
pulse() {
var m = this.video.video.duration;
var t = this.video.video.position;
if (!this.videopos.state.hover) {
this.videopos.attr["min"] = 0;
this.videopos.attr["max"] = m;
this.videopos.value = t;
}
var [th, tm, ts] = this.hms(t);
var [mh, mm, ms] = this.hms(m);
if (th == 0 && mh == 0) {
this.videotime.value = printf("%02d:%02d / %02d:%02d", tm, ts, mm, ms);
this.videotimecode = printf("%02dm%02ds", tm, ts);
} else {
this.videotime.value = printf("%d:%02d:%02d / %d:%02d:%02d", th, tm, ts, mh, mm, ms);
this.videotimecode = printf("%dh%02dm%02ds", th, tm, ts);
}
if (this.isVisible())
return true;
else
return false;
}
alignPlayerControls() {
this.timer(100ms, () => {
var videoWidth = this.video.video.renderingBox[2];
if (videoWidth == 0) return;
this.videocontrols.style.width = (videoWidth + 2) + "px";
this.videocontrols.classList.add("visible");
this.videocontrols.requestPaint();
});
}
}
class SwitchTab extends Element {
chatid = "";
plugid = 0;
pagetype = "im";
chat = null;
displayed = "";
this(props) {
this.pagetype = props.pagetype;
this.displayed = props.displayed;
this.chatid = props.chatid;
this.plugid = props.plugid;
this.chat = props.chat;
}
render() {
return <div class="tab">
<div class="tab-icon-group"><img class="tab-icon" src="" /><img class="tab-icon-add" src="" /></div>
<span class="tab-text">{this.displayed}</span>
<div class="tab-close"></div>
</div>;
}
componentDidMount() {
this.state.focusable = true;
this.paintForeground = function(gfx) {
var zindex = this.style["z-index"];
if (zindex && (parseInt(zindex) || 0) < 10000) return false;
var [p1, p2, p3, p4] = this.state.box("rect", "border");
gfx.strokeWidth = 1dip;
gfx.strokeStyle = Color.RGB(246, 246, 246);
gfx.translate(0.5, 0.5);
gfx.beginPath().moveTo(p1 - 0.5 + (this.index == 0 ? 0 : 1), p4).lineTo(p3 - 1.5, p4).stroke();
return false;
}
}
componentWillUnmount() {
this.paintForeground = null;
}
closePage(realize = false) {
if (this.pagetype == "im") {
if (realize) if (Native.RealizeEvents(this.chatid)) return true;
Native.CloseChatPage(this.chatid);
} else {
Native.DetachWindow(getPluginPage(this.plugid));
Native.ClosePluginPage(this.plugid);
}
return true;
}
updateDepth() {
if (this.state.checked)
this.style["z-index"] = 10000;
else
this.style["z-index"] = 9000 + tabs.length - this.index;
}
["on mouseenter"](e) {
if (settings.showHintsInChat && this.pagetype == "im" && !draggingSnap && !draggingTab) showHint(this, this.chatid);
}
["on mouseleave"](e) {
if (settings.showHintsInChat && this.pagetype == "im") hideHint();
}
["on ^mousedown"](e) {
if (e.mainButton)
this.startx = e.x + this.style["padding-left"].valueOf();
hideHint();
}
["on mousedown"](e) {
if (!e.mainButton) return false;
if (this.state.checked || !this.state.pressed) return false;
var chked = this.parent.$(":checked");
if (chked) {
chked.state.checked = false;
chked.chat.unsetChat();
}
this.state.checked = true;
updateTabsDepth();
this.chat.setChat();
this.chat.scrollTabToView();
}
["on ^mouseup"](e) {
if ((e.buttons & 4) || (e.mainButton && !skipMBtnUp && e.target && e.target.attr["class"] == "tab-close")) {
skipMBtnUp = false;
return this.closePage();
}
skipMBtnUp = false;
}
["on mouseup"](e) {
if (e.propButton && this.pagetype == "im") {
openContactMenu(this.chatid);
return true;
}
}
["on mousedragrequest"](e) {
draggingTab = true;
draggingTarget = this.cloneNode();
draggingTarget.style.visibility = "hidden";
var pw = tabs.state.box("width", "client");
var w = this.state.box("width");
var wb = this.state.box("width", "border") + this.style["margin-left"];
var h = this.state.box("height");
var l = this.state.box("left", "margin", tabs);
var first = this.$is(".tab:first-child");
var last = this.$is(".tab:last-child");
this.parent.append(draggingTarget);
this.swapWith(draggingTarget);
this.classList.add("capture");
tabs.state.capture(true);
this.style.set({
"left": l,
"width": w,
"height": h
});
tabs.on("mousemove", (me) => {
this.post(() => {
let x = me.x - this.startx;
let left = Math.min(Math.max(0, x), pw - wb);
this.style.left = left;
for (let tab of tabs.children) {
if (tab.index == draggingTarget.index) continue;
let tabLeft = tab.state.box("left", "margin", tabs);
let tabCenter = tabLeft + tab.state.box("width") / 2;
if ((tabLeft < left && left < tabCenter) ||
(tabLeft > left && left + w > tabCenter)) {
draggingTarget.detach();
tabs.insert(draggingTarget, draggingTarget.index > tab.index ? tab.index : tab.index + 1);
break;
}
}
});
});
view.doEvent("untilMouseUp");
tabs.state.capture(false);
tabs.off("mousemove");
let targetLeft = draggingTarget.state.box("left", "margin", tabs);
let targetDistance = targetLeft - this.style.left;
this.morphContent((progress) => {
this.style.left = targetLeft - targetDistance * (1 - progress);
if (progress >= 1) {
this.classList.remove("capture");
this.attr["style"] = "";
draggingTarget.swapWith(this);
draggingTarget.remove();
draggingTarget = null;
draggingTab = false;
let openedTabs = [];
for (let tab of tabs.children)
if (tab.pagetype === "im") openedTabs.push(tab.chatid);
Native.SaveTabsOrder(openedTabs);
return false;
}
return true;
}, { duration: 200ms, ease: "cubic-out" });
skipMBtnUp = true;
}
["on dblclick"](e) {
if (e.mainButton) return this.closePage();
}
}
function insertText(el, text) {
el.execCommand("edit:insert-text", text);
}
function fixHeart(text) {
return text.replace(/❤️/g, "❤");
}
class ChatEvent extends Element {
this(props, kids) {
var data = props.data;
this.encrypted = data.encrypted;
this.eccencrypted = data.eccencrypted;
this.prefix = data.prefix;
this.prefixCls = data.prefixCls;
this.patches = data.patches;
this.statusImg = data.statusImg;
this.statusImgExt = data.statusImgExt;
this.reactions = data.reactions;
this.myReaction = data.myReaction;
this.msg = data.msg;
this.embedded = data.embedded;
this.cls = data.cls;
this.time = data.time;
this.msgid = data.msgid;
this.eventImg = data.eventImg;
this.what = data.what;
this.when = data.when;
}
render() {
var encrypted = this.encrypted ? <div class="msgCryptImg" auto pic={this.eccencrypted ? "key.ecc" : "key"}></div> : "";
var prefix = this.prefix !== "" ? [<div class={this.prefixCls} key="prefix">{this.prefix}</div>] : [];
var statusImg = [];
if (this.statusImg != "") statusImg.push(<div class="msgStatusImg" auto pic={this.statusImg}></div>);
if (this.statusImgExt != "") statusImg.push(<div class="msgStatusExtImg" auto pic={this.statusImgExt}></div>);
var reactions = [];
if (this.reactions)
for (let reaction of this.reactions) if (reaction[1] > 0) {
reaction[0] = fixHeart(reaction[0]);
var reactCls = "msgReaction";
if (this.myReaction && this.myReaction == reaction[0]) reactCls += " my";
reactions.push(<div class={reactCls}>{reaction[0] + " "}<span>{reaction[1] > 1 ? reaction[1] : []}</span></div>);
}
if (!msgPreview)
prefix.push(<div class="msgReactBtn hidden" key="react" auto pic="reaction" hint={_("Select your reaction to this message")}></div>);
var body = this.msg + this.embedded;
return <div class={this.cls} time={this.time} msgid={this.msgid}>
<div class="msgTitle">
<div class="msgEventImg" auto pic={this.eventImg}></div>{encrypted}
<div class="msgWhat">{this.what}</div>
<div class="msgStatusImages">{statusImg}</div>
<div class="msgDate">{this.when}</div>{prefix}
</div>
<div class="msgBody" state-html={body}></div>
<div class="msgReactions">{reactions}</div>
</div>;
}
}
class Page extends Element {
chatid = "";
plugid = 0;
pagetype = "im";
displayed = "";
pendingScroll = ["none", false, null];
lastInputText = "";
enterCount = 0;
quoteIdx = 0;
selStart = 0;
topTime = 0;
msgOffset = 0;
bottomPos = -1;
topEvent = null;
topEventTime;
firstUnseenEvent = null;
firstUnreadEventTime = 0;
misspellings = null;
historyStartReached = false;
chatHasScroll = false;
selInfo = {
text: "",
startTime: "0",
endTime: "0",
wholeEvents: false
};
pageSettings = {
splitX: 0,
splitY: 0,
tiled: "",
positioned: ""
};
getChatId() {
return this.chatid;
}
getPlugId() {
return this.plugid;
}
this(props) {
this.pagetype = props.pagetype;
this.displayed = props.displayed;
if (this.pagetype == "plugin") {
this.plugid = props.plugid;
} else {
this.chatid = props.uid;
this.focused = props.focused;
}
}
render() {
var isPlug = this.pagetype == "plugin";
var contents = isPlug ? {} :
<frameset rows="*, 150dip" class="chatFrame" tabindex="-1">
<div class="chatBackground">
<div class="chatHist">
<div class="history hidden">
<div class="curtime"></div>
<div class="hr"></div>
</div>
<div class="serverHist inactive"></div>
<div class="chat"></div>
</div>
<div class="indicator hidden">{_("Back to latest messages")}</div>
<div class="loading"></div>
</div>
<splitter class="inputSplit" />
<div class="inputPanel">
<frameset cols="*, 100dip" class="inputFrame">
<plaintext class="input renderOption" spellcheck="false"></plaintext>
<splitter class="avatarSplit"/>
<div class="avatar hidden"><img /></div>
</frameset>
<div class="smartReplies"></div>
</div>
</frameset>;
return <div class="page" plugin={isPlug}>{contents}</div>
}
init() {
if (this.pagetype == "plugin") {
this.createTab();
Native.AttachWindow(this, this.plugid);
this.setCurrent();
return;
}
this.chat = this.$(".chat");
this.history = this.$(".history");
this.serverHist = this.$(".serverHist");
this.indicator = this.$(".indicator");
this.indicator.state.disabled = true;
this.indicator.setVisible(false);
this.curtime = this.$(".curtime");
this.loading = this.$(".loading");
this.chatHist = this.$(".chatHist");
this.chatBackground = this.$(".chatBackground");
this.input = this.$(".input");
this.inputSplit = this.$(".inputSplit");
this.inputPanel = this.$(".inputPanel");
this.avatarSplit = this.$(".avatarSplit");
this.chatFrame = this.$(".chatFrame");
this.inputFrame = this.$(".inputFrame");
this.smartReplies = this.$(".smartReplies");
this.avatar = this.$(".avatar");
this.avatarimg = this.avatar.$("> img");
if (!msgPreview) this.createTab();
if (pages.length == 1 || this.focused)
this.setCurrent();
else
updateTabsDepth();
this.serverHist.classList.remove("inactive");
this.history.classList.remove("hidden");
this.serverHist.attr["title"] = _("New messages are available in server history");
this.serverHist.on("click", (e) => {
e.target.classList.remove("visible");
this.startHistoryLoading();
});
this.indicator.on("click", (e) => {
this.scrollToFirstUnseen(true);
}).on("wheel", (e) => {
this.chatHist.dispatchEvent(e);
});
// this.on("^dblclick", ".codeCopyAll", function(e) { return true; });
this.on("^mousedown", ".codeCopyAll", (e, el) => this.mousedownCopyAll(e, el))
.on("^mousedown", ".msgTitle", (e, el) => {
if (e.mainButton && e.target && e.target.classList.contains("msgReactBtn")) {
var msg = e.target.$p(".msgFull");
var msgid = msg.attr["msgid"];
if (!msgid || (parseFloat(msgid) || 0) < 1000) {
showAlert("warning", "Cannot complete due to missing unique message ID");
} else {
myreactions.attr["msgid"] = msgid;
myreactions.attr["chatid"] = this.chatid;
var my = Native.GetMyReaction(msgid);
for (let option of myreactions.$$("> div")) {
var del = option.$("> div.delete");
if (parseInt(option.attr["option"]) == my) {
option.classList.add("my");
del.setVisible(true);
} else {
option.classList.remove("my");
del.setVisible(false);
}
}
e.target.popup(myreactions, {
anchorAt: 8,
popupAt: 2
});
}
return true;
} else return this.mousedownTitle(e, el);
}).on("^mouseup", ".msgBody", (e, el) => {
if (e.mainButton) {
this.saveSelection();
if (el.selection.toString() == "") document.body.state.focus = true;
}
});
this.chatHist.on("^mousedown", (e) => {
if (e.mainButton) {
this.clearSelection(e.ctrlKey || e.shiftKey);
this.saveSelection();
};
}).on("^mouseup", (e) => {
if (msgPreview) return false;
function isUINLink(link) {
return link && link.toLowerCase().indexOf("uin:") === 0;
}
if (e.propButton || (e.mainButton && e.target.tag == "a" && isUINLink(e.target.attr["href"]))) {
if (videoview && videoview.isVisible()) return true;
var isLink = false;
var isImage = false;
var isICQImage = false;
var msg = null;
if (e.target.attr["class"] == "msgFull") {
rightClickedTime = e.target.attr["time"];
rightClickedData = e.target.attr["msgid"];
msg = e.target;
} else {
msg = e.target.$p(".msgFull");
if (msg) {
rightClickedTime = msg.attr["time"];
rightClickedData = msg.attr["msgid"];
}
}
var isValidMsgID = rightClickedData !== null && (parseFloat(rightClickedData) || 0) > 10000;
if (e.target.tag == "a") {
isLink = true;
rightClickedData = e.target.attr["href"];
} else if (e.target.tag == "img" || e.target.tag == "lottie") {
isImage = true;
rightClickedData = e.target.attr["src"];
isICQImage = rightClickedData.toLowerCase().indexOf(icqfilelnk) >= 0;
}
var isUIN = isLink && isUINLink(rightClickedData);
var selText = this.selInfo.text;
var selWhole = this.selInfo.wholeEvents && this.selInfo.startTime != "0" && this.selInfo.endTime != "0";
var isJustMsg = !isLink && !isImage && !selWhole && rightClickedData !== null && msg !== null;
histmenu.hm_contactmenu.setVisible(isUIN);
histmenu.hm_copylink.setVisible(isLink);
histmenu.hm_copy.setVisible(selText != "");
histmenu.hm_savepic.setVisible(isImage);
histmenu.hm_stickerpack.setVisible(isICQImage);
histmenu.hm_selectall.state.disabled = this.chat.length == 0;
histmenu.hm_resethist.setVisible(this.chat.children.length > settings.msgBuffer);
histmenu.hm_viewinwin.state.disabled = selText == "" && rightClickedTime == "0";
histmenu.hm_saveas.setVisible(selText !== "");
histmenu.hm_saveashtml.setVisible(selWhole);
histmenu.hm_addtofav.setVisible(isLink && rightClickedData.indexOf("link:") == 0);
histmenu.hm_edit.setVisible(isJustMsg && msg.classList.contains("my") && !msg.classList.contains("binary"));
histmenu.hm_edit.state.disabled = !isValidMsgID;
histmenu.hm_delete.setVisible(selWhole);
histmenu.hm_reactions.setVisible(settings.showReactions && isJustMsg);
histmenu.hm_reactions.state.disabled = !isValidMsgID;
histmenu.hm_hr_textsel.setVisible(selText != "");
histmenu.hm_antispam.setVisible(selText != "");
histmenu.hm_viewinfo.setVisible(rightClickedTime != "0");
histmenu.hm_imgsmiles.$("> div").style.opacity = settings.showSmiles ? 1.0 : 0.5;
histmenu.hm_reltimes.$("> div").style.opacity = settings.showRelTimes ? 1.0 : 0.5;
var [x, y] = view.box("position", "cursor");
this.popup(histmenu.self, {
x: x,
y: y
});
}
});
if (msgPreview) return;
this.chatHist.on("scroll", (e) => this.checkScrollPositionThrottled())
.on("sizechange", () => {
this.checkScrollPositionThrottled(false);
this.checkScrollBackground();
}).on("scrollsliderpress", (e) => {
scrollbarActive = true;
}).on("scrollsliderrelease", (e) => {
scrollbarActive = false;
if (!this.historyStartReached && this.chatHist.scrollTop <= 0) this.loadHistory();
});
this.avatar.on("mouseup", (e) => {
if (e.propButton && this.pagetype == "im") {
openContactMenu(this.chatid);
return true;
}
});
this.input.on("mousedragrequest", e => true)
.on("dragaccept", (e) => { return e.detail?.dataType !== "html" })
.on("drag", () => false)
.on("dragenter", function() {
this.state.disabled = true;
document.body.isDragActive = true;
return false;
}).on("dragleave", function() {
this.state.disabled = false;
document.body.hideDrags();
return false;
}).on("drop", function() {
this.state.disabled = false;
document.body.isDragActive = false;
return false;
}).on("change", () => this.changeInput())
.on("focus", (e) => this.enterCount = 0)
.on("blur", (e) => this.enterCount = 0)
.on("keydown", (e) => {
if (e.keyCode == Event.VK_RETURN) {
this.enterCount++;
if (this.enterCount == settings.sendOnEnter) {
this.input.value = this.input.value.slice(0, -settings.sendOnEnter * 2 + 1);
this.sendMessage(0);
this.enterCount = 0;
return true;
}
if (e.ctrlKey) {
this.sendMessage(0);
this.enterCount = 0;
return true;
}
return false;
} else {
this.enterCount = 0;
}
if (e.altKey) {
if (e.keyCode == Event.VK_S) this.sendMessage(0);
return true;
}
}).on("^keypress", (e) => {
if (e.ctrlKey && e.key == " ") {
var [x, y] = this.input.state.box("position", "caret", "window");
this.input.popup(codemenu, {
x: x,
y: y + 12pt.valueOf()
});
return true;
}
if (e.altKey) return true;
}).on("contextmenusetup", (e) => {
var suggest = e.source.$("#suggest");
suggest.clear();
if (!this.misspellings) return;
var pos = this.input.getCaretPos();
var word = null, st, len;
for (let misspell of this.misspellings) if (misspell)
if (pos >= misspell[0] && pos <= misspell[0] + misspell[1]) {
st = misspell[0];
len = misspell[1];
word = this.input.value.substr(st, len);
break;
}
if (word) {
var suggestions = Native.GetSpellingSuggestions(word);
if (suggestions && suggestions.length > 0) {
for (let suggestion of suggestions)
suggest.append(<li start={st} len={len}><label>{suggestion}</label></li>);
suggest.append(<hr/>);
}
}
}).on("click", "#suggest", (e) => {
var st = parseInt(e.target.attr["start"]);
var len = parseInt(e.target.attr["len"]);
this.input.saveCaretPos();
this.input.value = this.input.value.splice(st, len, e.target.$("label").text);
this.input.restoreCaretPos();
this.input.state.focus = true;
this.changeInput();
});
this.inputSplit.on("mouseup", () => this.saveAndUpdateSplitter());
this.avatarSplit.on("mouseup", () => this.saveAndUpdateSplitter());
this.smartReplies.on("click", "div", (e, el) => {
this.input.value = "";
this.addToInput(el.text);
});
this.updateSmartReplies();
this.updateAvatar();
Native.RequestChatPageSettings(this.chatid);
if (this.chat.children.length == 0 && !this.historyStartReached) this.loadHistory();
}
["on ^wheel"](e) {
if (this.pagetype == "plugin") return false;
if (this.isHistoryLoading()) return true;
if (this.input.state.hover) return false;
if (videoview && videoview.isVisible()) return true;
if (e.altKey) {
this.scrollEvent(e.deltaY < 0 ? -1 : +1);
skipAltUp = true;
return true;
}
}
["on ^keyup"](e) {
if (this.pagetype == "plugin") return false;
if (skipAltUp && e.altKey && (e.keyCode == Event.VK_LEFT_ALT || e.keyCode == Event.VK_MENU)) {
skipAltUp = false;
return true;
}
if (e.shiftKey && e.target.attr["class"] == "msgBody") this.saveSelection();
}
["on ^keydown"](e) {
if (this.pagetype == "plugin" || e.shiftKey) return false;
if ((videoview && videoview.isVisible()) || (overlay && overlay.state.visible)) return false;
// Switch chats on Alt + 1..9
if (e.altKey && e.keyCode >= 49 && e.keyCode <= 57)
if (tabs.children[e.keyCode - 49])
tabs.children[e.keyCode - 49].chat.setCurrent();
if (this.isHistoryLoading()) return true;
if ([Event.VK_UP, Event.VK_DOWN, Event.VK_PRIOR, Event.VK_NEXT].includes(e.keyCode))
if (e.ctrlKey)
switch (e.keyCode) {
case Event.VK_UP: this.scrollLine(-1); return true;
case Event.VK_DOWN: this.scrollLine(+1); return true;
case Event.VK_PRIOR: this.scrollLine(-10); return true;
case Event.VK_NEXT: this.scrollLine(+10); return true;
} else if (e.altKey)
switch (e.keyCode) {
case Event.VK_UP: this.scrollEvent(-1); return true;
case Event.VK_DOWN: this.scrollEvent(+1); return true;
case Event.VK_PRIOR: this.scrollEvent(-5); return true;
case Event.VK_NEXT: this.scrollEvent(+5); return true;
}
if (e.altKey && [Event.VK_HOME, Event.VK_END].includes(e.keyCode))
switch (e.keyCode) {
case Event.VK_HOME: this.scrollToTop(true); return true;
case Event.VK_END: this.scrollToBottom(true); return true;
}
// Override default copy when selecting in chat
if (!this.input.state.focus && e.ctrlKey && e.keyCode == Event.VK_C) {
this.copySelected()
return true;
}
}
updateSettings(sets) {
if (this.pagetype == "plugin") return false;
this.pageSettings = sets;
reloadImage(this.pageSettings.tiled);
if (this.pageSettings.tiled) this.chatBackground.style.set({
"background-image": "url(" + this.pageSettings.tiled + ")",
"background-repeat": "repeat",
"background-position": "center center"
}); else this.chatBackground.style.set({
"background-image": "none",
"background-repeat": "no-repeat"
});
reloadImage(this.pageSettings.positioned);
this.checkScrollBackground(true);
if (this.pageSettings.splitX > 0) {
//this.inputFrame.frameset.state = [1fx, Length.px(this.pageSettings.splitX)];
this.inputFrame.attr["cols"] = "*, " + this.pageSettings.splitX + "px";
this.avatar.style.width = this.pageSettings.splitX + "px";
}
if (this.pageSettings.splitY > 0) {
this.chatFrame.attr["rows"] = "*, " + this.pageSettings.splitY + "px";
this.inputPanel.style.height = this.pageSettings.splitY + "px";
}
this.updateSplitterWidth();
}
saveAndUpdateSplitter(e) {
if (settings.showAvatar) Native.StoreSplit(0, this.avatar.state.box("width"));
Native.StoreSplit(1, this.inputPanel.state.box("height"));
this.updateSplitterWidth();
}
checkScrollPosition(loadHist = true) {
if (msgPreview || !this.parent) return;
var atBottom = this.isAtBottom();
if (atBottom) this.firstUnseenEvent = null;
if (this.indicator.state.disabled !== atBottom) {
if (atBottom) {
this.indicator.on("transitionend", (e) => {
if (this.indicator.style.opacity > 0) return;
this.indicator.off("transitionend");
this.indicator.setVisible(false);
});
this.indicator.classList.add("hidden");
} else {
this.indicator.off("transitionend");
this.indicator.setVisible(true);
this.indicator.classList.remove("hidden");
}
this.indicator.state.disabled = atBottom;
}
if (loadHist && !this.historyStartReached && !scrollbarActive && this.chatHist.scrollTop <= 0) this.loadHistory();
}
checkScrollPositionThrottled = this.checkScrollPosition.throttle(200ms, true);
// Update background tile
checkScrollBackground(force = false) {
if (msgPreview || this.pagetype == "plugin") return;
if (!force) {
var hasScroll = this.chatHist.state.box("height", "content", "window") > this.chatHist.state.box("height");
if (hasScroll == this.chatHasScroll) return;
this.chatHasScroll = hasScroll;
}
if (!this.pageSettings.positioned || this.pageSettings.positioned == "") {
this.chatHist.style["background-image"] = "none";
return;
};
var bpos = "right bottom";
switch (this.pageSettings.positioned.charAt(this.pageSettings.positioned.length - 1)) {
case "1": bpos = "left top"; break;
case "2": bpos = (this.chatHasScroll ? "right system-scrollbar-width" : "right") + " top"; break;
case "3": bpos = "left bottom"; break;
case "4": bpos = (this.chatHasScroll ? "right system-scrollbar-width" : "right") + " bottom"; break;
}
this.chatHist.style.set({
"background-image": "url(" + this.pageSettings.positioned + ")",
"background-position": bpos
});
}
isAtBottom() {
return this.chatHist.scrollBottom <= (this.chat.last ? (this.chat.last.$(".msgBody").state?.box("height", "margin") || 0) : 0);
}
isInView(el) {
var [elLeft, elTop, elRight, elBottom] = el.state.box("rect", "border", this.chatHist);
var chatHeight = this.chatHist.state.box("height", "client");
if (elTop >= 0 && elBottom <= chatHeight)
return 1;
else if (!(elBottom <= 0 || elTop >= chatHeight))
return 2;
else
return 0;
}
getImages(kind, prepend = false, empty = false) {
this.bottomPos = prepend && !empty || kind === "embedded" ? -1 : (empty ? 0 : this.chatHist.scrollBottom);
//console.log("getImages scrollBottom:", this.chatHist.scrollBottom, this.bottomPos);
var imgs = this.chat.$$("img[data-action], lottie[data-action]");
for (let img of imgs) if (img)
if (kind == "both" ||
(kind == "embedded" && img.classList.contains("embeddedImg")) ||
(kind == "linked" && img.classList.contains("linkedImg"))) getImg(img);
}
mousedownCopyAll(e, el) {
if (!e.mainButton) return false;
this.clearSelection(e.ctrlKey || e.shiftKey);
var code = el.parent.$("source-code");
var first = code.first?.firstChild;
var last = code.last?.lastChild;
if (first && last)
code.selection.setBaseAndExtent(first, 0, last, last.length);
else
code.execCommand("edit:selectall");
return true;
}
mousedownTitle(e, el) {
if (!e.mainButton) return false;
var chatEvent = e.target.$p(".msgFull");
if (e.target.classList.contains("msgMulti") && e.target.classList.contains("updated") && chatEvent.patches) {
patches.clear();
var delSelfStr = _("Message was removed from server (for self)");
var delAllStr = _("Message was removed from server (for all)");
var noDataStr = _("No data");
for (let patch of chatEvent.patches) {
var removedSelf = patch[0] == "delete"
var removedAll = patch[0] == "modify";
var removed = removedSelf || removedAll;
var empty = patch[1] === "";
var text = "";
if (removedSelf)
text = delSelfStr;
else if (removedAll)
text = delAllStr;
else if (empty)
text = noDataStr;
else {
text = fixHeart(patch[1]);
if (text.length > 300) text = text.substr(0, 300) + "…";
}
patches.append(<div action={patch[0]} class={removed || empty ? "grayedout" : ""}><div pic={removed ? "delete" : "refresh"}></div><div class="patchText">{text}</div></div>);
}
var pics = patches.$$("div[pic]");
for (let pic of pics) pic.setupIcon();
e.target.popup(patches, {
anchorAt: 7,
popupAt: 1
});
if (patches.last) patches.last.scrollIntoView({
block : "nearest",
behavior: "instant"
});
return true;
}
if ((!e.shiftKey && !e.ctrlKey) || this.selStart == 0) {
this.clearSelection();
this.selStart = parseFloat(el.parent.attr["time"]) || 0;
selectEvent(el.parent);
} else {
var selEnd = parseFloat(el.parent.attr["time"]) || 0;
if (e.ctrlKey) {
if (isEventSelected(el.parent))
deselectEvent(el.parent);
else
selectEvent(el.parent);
} else if (e.shiftKey)
for (let msg of this.chat.children) {
var msgTime = parseFloat(msg.attr["time"]) || 0;
if ((this.selStart <= selEnd && msgTime >= this.selStart && msgTime <= selEnd) ||
(this.selStart > selEnd && msgTime <= this.selStart && msgTime >= selEnd))
selectEvent(msg);
else
deselectEvent(msg);
}
}
this.saveSelection();
return true;
}
updateSpelling(errors) {
if (this.pagetype == "plugin") return;
this.misspellings = errors ? deepCopy(errors) : null;
var node = this.input.first;
if (node) {
const range = new Range();
range.setStart(node, 0);
range.setEnd(this.input.last, this.input.last?.textContent.length ?? 0);
range.clearMark("wrongword");
}
if (!node || !errors || errors.length == 0) return;
var cnt = 0;
do {
if (errors[cnt]) {
do {
if (errors[cnt][0] > node.textContent.length) {
for (var i = cnt; i < errors.length; i++)
if (errors[i]) errors[i][0] -= node.textContent.length + 2;
node = node.next;
} else break;
} while (node);
if (node) {
var tNode = node.firstChild;
const range = new Range();
range.setStart(tNode, errors[cnt][0]);
range.setEnd(tNode, errors[cnt][0] + errors[cnt][1]);
range.applyMark("wrongword");
}
}
cnt++;
} while (cnt < errors.length);
}
sendMessage(opt = 0) {
let msg = this.input.value;
if (opt > 0 && msg.trim() === "") {
this.input.state.focus = true;
showAlert("warning", "Can't send an empty message");
return;
}
var UINs = [];
if (opt == 1) {
UINs = openUINList("Send multiple", "Send message", ["sco_multi", "sco_groups", "sco_predefined"], this.chatid);
if (!UINs || UINs.length == 0) {
if (!findWindow("uinlist"))
this.input.state.focus = true;
return;
}
}
this.input.state.focus = true;
var sent = Native.SendChatMessage(opt, this.chatid, msg, UINs);
if (!chatButtons.singleBtn.state.checked && this.input.parent) {
if (sent === true) {
this.input.value = "";
this.input.state.focus = true;
this.input.execCommand("navigate:start"); // focus bug fix
}
this.changeInput();
}
}
updateCharCount() {
$("#sbChars").text = this.input.value.length > 0 ? _("Chars") + ": " + this.input.value.length : "";
}
changeInput() {
this.quoteIdx = 0;
this.updateCharCount();
Native.InputChangedFor(this.chatid, this.input.value);
}
addToInput(text) {
if (text === "") return;
insertText(this.input, text);
this.input.state.focus = true;
this.changeInput();
}
createTab() {
this.tab = Element.create(<SwitchTab pagetype={this.pagetype} chatid={this.chatid} plugid={this.plugid} chat={this} displayed={this.displayed} />);
tabs.append(this.tab);
}
setChat() {
for (let page of pages.children)
page.state.current = this.pagetype == "plugin" ? page.plugid == this.plugid : page.chatid == this.chatid;
if (msgPreview) return;
setupChatButtons(this.pagetype == "plugin");
if (this.pagetype == "plugin") {
Native.PluginPageSelected(this.plugid);
this.state.focus = true;
} else {
this.updateCharCount();
this.updateSplitterWidth();
Native.ChatPageSelected(this.chatid, this.input.value);
this.checkScrollBackground(true);
if (this.pendingScroll[0] == "tobottom")
this.scrollToBottom(this.pendingScroll[1]);
else if (this.pendingScroll[0] == "tounseen")
this.scrollToFirstUnseen(this.pendingScroll[1]);
else if (this.pendingScroll[0] == "movetotime")
this.moveToTime(this.pendingScroll[2], this.pendingScroll[1]);
this.pendingScroll = ["none", false];
this.post(() => this.input.state.focus = true);
//this.flushPaint();
}
}
unsetChat() {
if (msgPreview) return;
if (this.pagetype == "plugin")
Native.PluginPageDeselected(this.plugid);
else
Native.ChatPageDeselected(this.chatid);
}
setCurrent() {
if (msgPreview) {
this.setChat();
return;
}
var isPlug = this.pagetype == "plugin";
var chked = tabs.$(":checked");
if (chked) {
if (isPlug ? chked.plugid == this.plugid : chked.chatid == this.chatid) return;
chked.state.checked = false;
chked.chat.unsetChat();
}
for (let tab of tabs.children)
if (isPlug ? tab.plugid == this.plugid : tab.chatid == this.chatid) {
tab.state.checked = true;
updateTabsDepth();
this.scrollTabToView();
this.setChat();
break;
}
}
scrollTabToView() {
this.tab.scrollIntoView({ behavior: "smooth" });
}
redrawTab(caption, hash, hashadd = 0) {
if (!this.tab) return;
var icon = this.tab.$(".tab-icon");
var iconGroup = this.tab.$(".tab-icon-group");
if (hash && hash !== 0) {
icon.attr["src"] = "tabicon:" + printf("%u", hash);
iconGroup.classList.remove("hidden");
} else {
iconGroup.classList.add("hidden");
}
var iconadd = this.tab.$(".tab-icon-add");
if (hashadd && hashadd !== 0) {
iconadd.attr["src"] = "tabicon:" + printf("%u", hashadd);
iconadd.classList.remove("hidden");
} else {
iconadd.classList.add("hidden");
}
this.tab.$(".tab-text").text = caption;
}
closeTab() {
hideHint();
if (tabs.length == 1 || this.tab.index !== tabs.length - 1) {
// remove single and any middle tabs without animation
this.tab.remove();
return;
}
this.tab.state.focusable = false;
this.tab.state.disabled = true;
this.tab.state.checked = false;
this.tab.flushPaint();
var [x, y] = this.tab.state.box("position", "border", "parent");
var pl = backTabs.style["padding-left"].valueOf();
var pt = backTabs.style["padding-top"].valueOf();
var [w, h] = this.tab.state.box("dimension", "inner");
x += pl; y += pt;
this.tab.detach();
backTabs.append(this.tab);
this.tab.style.set({
"position": "absolute",
"margin": 0,
"left": x + "px",
"top": y + "px",
"width": w + "px",
"height": h + "px"
});
this.tab.morphContent((progress) => {
this.tab.style.set({
"opacity": 1.0 - progress,
//"width": w - 20 * progress + "px"
"top": (y + 10 * progress) + "px"
});
if (progress >= 1) {
this.tab.remove();
return false;
}
return true;
}, { duration: 300ms, ease: "cubic-out" });
}
close() {
if (msgPreview) {
this.remove();
return;
}
if (this.pagetype == "im" && videoview) {
if (videoview.init) videoview.closeVideo();
videoview.detach();
}
var i = this.tab.index;
this.closeTab();
this.remove();
if (i >= tabs.length) i = tabs.length - 1;
if (i < 0) {
closeForm();
} else {
var chked = tabs.$(":checked");
if (chked) {
chked.state.checked = false;
chked.chat.unsetChat();
}
for (let tab of tabs.children)
if (tabs.length == 1 || tab.index == i) {
tab.state.checked = true;
updateTabsDepth();
tab.chat.setChat();
tab.chat.scrollTabToView();
break;
}
}
}
initTopTime() {
if (this.chat.first)
this.topTime = parseFloat(this.chat.first.attr["time"]) || 0;
if (this.topTime == 0) {
var now = new Date();
this.topTime = unix2delphi(now.valueOf() + now.getTimezoneOffset());
}
}
updateCurTime() {
var date = new Date(delphi2unix(this.topTime));
var weekDay = date.getDay();
weekDay = weekDay === 0 ? 6 : weekDay - 1;
this.curtime.html = CommonNative.GetWeekDayByNumber(weekDay) + ", " + date.getDate() + " " + CommonNative.GetMonthNameByNumber(date.getMonth()) + " " + date.getFullYear();
this.curtime.style.display = "block";
}
updateSmiles() {
if (settings.showSmiles) {
pages.classList.remove("smileAsText");
pages.classList.add("smileAsImg");
} else {
pages.classList.remove("smileAsImg");
pages.classList.add("smileAsText");
}
}
showServerHistoryNotif() {
this.serverHist.classList.add("visible");
}
startHistoryLoading() {
this.loading.classList.add("visible");
this.curtime.html = _("Loading history...");
this.curtime.style.display = "block";
}
endHistoryLoading() {
this.loading.classList.remove("visible");
}
isHistoryLoading() {
return this.loading.classList.contains("visible");
}
hideHistory() {
this.historyStartReached = true;
this.endHistoryLoading();
this.history.classList.add("removed");
}
loadHistory() {
this.startHistoryLoading();
var localMsgOffset = this.msgOffset;
this.msgOffset += settings.msgBuffer;
this.post(() => Native.LoadHistory(this.chatid, localMsgOffset, settings.msgBuffer));
}
resetHistory() {
this.clearEvents();
this.loadHistory();
}
rememberTopEvent() {
if (!this.chat) return;
this.topEvent = this.chat.first;
if (this.topEvent) this.topEventTime = this.topEvent.attr["time"];
}
/*
restoreTopEvent() {
var msg;
if (this.topEventTime)
msg = this.chat.$(".msgFull[time=\"{this.topEventTime}\"]");
else
msg = this.chat.last;
if (msg)
this.scrollEventToView(msg, true, false);
}
*/
addEvents(events, prepend) {
var preScrollBottom = this.isAtBottom();
var preEmpty = this.chat.children.length == 0;
if (prepend) this.rememberTopEvent();
if (events && events.length > 0) {
var data = [];
for (let event of events) {
//if (event.msgid !== "" && event.msgid !== "0" && this.chat.$("> .msgFull[msgid=\"" + event.msgid + "\"]")) continue;
data.push(<ChatEvent data={event} />);
if (!prepend && event.writeHist) this.msgOffset++;
}
if (prepend)
this.chat.prepend(data);
else
this.chat.append(data);
}
//var hiddenMsgs = this.chat.$$(> .msgFull.hidden);
//for (let hiddenMsg of hiddenMsgs)
//hiddenMsg.classList.remove("hidden");
if (msgPreview) hideSearchHere();
this.endHistoryLoading();
this.initTopTime();
this.updateCurTime();
this.updateSmiles();
this.getImages("embedded");
this.checkScrollBackground();
if (prepend) {
if (this.topEvent) {
var prevMsg = this.topEvent.prior;
var ht = this.chatHist.scrollTop - 2dip.valueOf();
while (prevMsg) {
ht += prevMsg.state.box("height", "margin");
prevMsg = prevMsg.prior;
}
this.chatHist.scrollToEx(0, ht, false);
} else this.scrollToBottom(false);
} else {
var noFirstUnseen = this.firstUnseenEvent == null;
var evcnt = events ? events.length : 0;
var evlast = this.chat.last;
if (evcnt > 0) {
delete lastSmartReplies[this.chatid];
this.smartReplies.clear();
}
while (evlast && evcnt > 0) {
if (noFirstUnseen && this.isInView(evlast) > 0) this.firstUnseenEvent = evlast;
evlast = evlast.prior;
evcnt--;
}
if (evlast && evlast.next)
if (evlast.next.classList.contains("my"))
this.firstUnreadEventTime = 0;
else if ((!isActive || getCurrentPage() !== this) && this.firstUnreadEventTime === 0)
this.firstUnreadEventTime = evlast.next.attr["time"];
if (!msgPreview && preScrollBottom && this.chat.children.length > 0)
this.scrollToBottom(false);
else
this.checkScrollPosition(false);
}
this.getImages("linked", prepend, preEmpty);
this.redrawFirstUnreadEvent();
}
updateEvents(events) {
if (!events || events.length == 0) return;
for (let event of events)
if (event.msgid !== "" && event.msgid !== "0") {
var msg = this.chat.$("> .msgFull[msgid=\"" + event.msgid + "\"]");
if (msg) {
msg.patch(<ChatEvent data={event} />);
if (msg == this.chat.last && this.isAtBottom()) this.scrollToBottom(true);
}
}
this.getImages("both");
}
redrawFirstUnreadEvent() {
var currentLast = this.chat.$("> .msgFull.lastevent");
if (currentLast) currentLast.classList.remove("lastevent");
if (this.firstUnreadEventTime !== 0 && this.firstUnreadEventTime !== "") {
var msg = this.chat.$("> .msgFull[time=\"" + this.firstUnreadEventTime + "\"]");
if (msg) msg.classList.add("lastevent");
}
}
setFirstUnreadEvent(time) {
if (this.firstUnreadEventTime !== 0) return;
this.firstUnreadEventTime = time;
this.redrawFirstUnreadEvent();
}
clearEvents() {
if (videoview && videoview.isVisible()) videoview.closeVideo();
this.chat.clear();
this.msgOffset = 0;
}
scrollEvent(cnt) {
for (let msg of this.chat.children) {
var vis = this.isInView(msg);
if (vis > 0) {
if (vis == 2) {
if (this.chatHist.state.animating) {
if (cnt < 0) cnt--; else if (cnt > 0) cnt++;
} else if (cnt < 0) cnt++;
}
if (cnt > 0) {
for (var i = 1; i <= cnt; i++)
if (msg.next) msg = msg.next;
} else if (cnt < 0) {
for (var i = cnt; i <= -1; i++)
if (msg.prior) msg = msg.prior;
}
if (msg) this.scrollEventToView(msg, true, settings.animatedScroll);
break;
}
}
}
scrollLine(cnt) {
// 1 step = 10% of chat height
this.chatHist.scrollToEx(0, parseInt(this.chatHist.scrollTop + cnt * this.chatHist.state.box("height") / 10), settings.animatedScroll);
}
// scrollWheel(direction) {
// // 1 step = 10% of chat height
// if (direction < 0) direction = -1;
// if (direction > 0) direction = 1;
// //if ((this.chatHist.scroll(#top) <= 0 && direction < 0) || (this.chatHist.scroll(#bottom) <= 0 && direction > 0)) return;
// var y = this.chatHist.scrollTop + direction * settings.wheelVelocity * this.chatHist.state.box("height") / (msgPreview ? 5 : 10);
// this.chatHist.scrollToEx(0, y, settings.animatedScroll);
// }
scrollEventToView(el, toTop, animate) {
var topShift = this.history.state.visible ? this.history.state.box("height", "margin") : 0;
var scrollPosY = topShift + (toTop ? el.state.box("top", "margin", "parent") : el.state.box("bottom", "margin", "parent") - this.chatHist.clientHeight);
scrollPosY = Math.min(Math.max(0, scrollPosY), this.chatHist.scrollHeight - this.chatHist.clientHeight);
this.chatHist.scrollToEx(0, scrollPosY, animate);
}
scrollToTop(animate) {
this.chatHist.scrollToEx(0, 0, animate && settings.animatedScroll);
}
scrollToBottom(animate) {
if (getCurrentPage() !== this) {
if (this.pendingScroll[0] == "movetotime") return;
this.pendingScroll = ["tobottom", animate];
return;
}
if (this.chat.length > 0 && this.chat.last)
this.scrollEventToView(this.chat.last, false, animate && settings.animatedScroll);
}
scrollToFirstUnseen(animate) {
if (getCurrentPage() !== this) {
this.pendingScroll = ["tounseen", animate];
return;
}
if (this.firstUnseenEvent == null)
this.scrollToBottom(animate);
else
this.scrollEventToView(this.firstUnseenEvent, true, animate && settings.animatedScroll)
this.firstUnseenEvent = null;
}
getLastEventTime() {
return this.chat.last ? this.chat.last.attr["time"] : 0;
}
moveToTime(time, fast) {
if (getCurrentPage() !== this) {
this.pendingScroll = ["movetotime", fast, time];
return;
}
if (this.chat.children.length > 0)
for (let msg of this.chat.children)
if (msg.attr["time"] == time) {
this.scrollEventToView(msg, true, !fast && settings.animatedScroll);
break;
}
}
selectAll() {
if (this.chat.children.length > 0)
this.setSelection(this.chat.first.attr["time"], this.chat.last.attr["time"]);
}
clearSelection(wholeOnly = false) {
if (!wholeOnly) {
var bodies = this.chat.$$(".msgBody");
for (let body of bodies)
body.selection.collapseToStart();
var codeBlocks = this.chat.$$("source-code");
for (let codeBlock of codeBlocks)
codeBlock.selection.collapseToStart();
}
this.selStart = 0;
for (let el of this.chat.children)
deselectEvent(el);
}
setSelection(start, end) {
this.clearSelection();
for (let el of this.chat.children) {
var msgTime = parseFloat(el.attr["time"]) || 0;
if (msgTime >= (parseFloat(start) || 0) && msgTime <= (parseFloat(end) || 0)) selectEvent(el);
}
this.selStart = parseFloat(start) || 0;
this.saveSelection();
}
getTextNodes(node) {
var result = "";
for (var child = node.firstChild; child != null; child = child.nextSibling)
if (child.nodeType == 3 || (child.state.visible && child.length == 0))
result += child.nodeType == 3 ? child.textContent : (child.tag == "text" ? child.text + "\r\n" : child.text);
else if (child.state.visible)
result += this.getTextNodes(child);
return result;
}
getSelectedTextInfo() {
var str = "";
var startHistNode = "0";
var endHistNode = "0";
var wholeEvents = false;
if (this.selStart > 0) {
wholeEvents = true;
var nodes = this.chat.$$("> .msgFull.selected");
if (nodes.length > 0) {
startHistNode = nodes.first.attr["time"];
endHistNode = nodes.last.attr["time"];
}
for (let node of nodes) {
if (str.length > 0) str += "\r\n";
var dateText = node.$(".msgDate").text;
var whatText = node.$(".msgWhat").text;
var multiText = "";
var multi = node.$(".msgMulti");
if (multi != null) {
multiText = multi.text;
if (multiText.length > 0) multiText = " " + multiText;
}
var bodyText = this.getTextNodes(node.$(".msgBody"));
str += dateText + ", " + whatText + multiText + "\r\n" + bodyText;
}
} else {
var bodies = this.chat.$$(".msgBody");
for (let body of bodies) {
if (body.selection.toString() != null && body.selection.toString() !== "") {
if (str !== "") str += "\r\n";
str += body.selection.toString();
}
var codeBlocks = body.$$("source-code");
for (let codeBlock of codeBlocks)
if (codeBlock.plaintext.selectionText != null && codeBlock.plaintext.selectionText !== "") {
if (str !== "") str += "\r\n";
str += codeBlock.plaintext.selectionText;
}
}
}
return {
text: str.replace(/\r?\n/g, "\r\n"),
startTime: startHistNode === undefined ? "0" : startHistNode,
endTime: endHistNode === undefined ? "0" : endHistNode,
wholeEvents: wholeEvents
};
}
saveSelection() {
this.selInfo = this.getSelectedTextInfo();
Native.UpdateSelection(this.selInfo.text, this.selInfo.startTime, this.selInfo.endTime, this.selInfo.wholeEvents);
if (settings.autoCopy) this.copySelected();
}
copySelected() {
var str = this.selInfo.text;
if (str) Clipboard.writeText(str);
}
deleteEvents(start, end) {
var startTime = parseFloat(start) || 0;
var endTime = parseFloat(end) || 0;
var msgs = [...this.chat.children];
for (let msg of msgs) {
var when = parseFloat(msg.attr["time"]) || 0;
if (when >= startTime && when <= endTime) {
msg.remove();
this.msgOffset--;
}
}
}
deleteEvent(msgid) {
var msgs = [...this.chat.children];
for (let msg of msgs)
if (msgid === msg.attr["msgid"]) {
msg.remove();
this.msgOffset--;
}
}
updateMsgStatus(status) {
var msg = this.chat.$("> .msgFull[time=\"" + status.when + "\"]");
if (msg) {
msg.attr["msgid"] = status.msgid;
var img = msg.$(".msgEventImg");
if (img) img.applySprite(status.eventimg);
}
}
updateSplitterWidth() {
this.post(() => {
var m = this.inputPanel.style["padding-left"].valueOf();
var chatWidth = this.state.box("width", "border");
var inputWidth = this.input.state.box("width", "border");
if (chatWidth == 0 || inputWidth == 0) return;
this.inputSplit.style["margin-right"] = (chatWidth - inputWidth - m) + "px";
});
}
quote(qs, limitWidth = false) {
if (qs && qs.length > 0) {
this.processQuote(qs, true, limitWidth);
} else {
if (settings.quoteSelected && this.selInfo.text != "") {
this.processQuote(this.selInfo.text);
} else {
// Save original reply at the beginning of a quoting cycle
if (this.quoteIdx == 0) this.lastInputText = this.input.value;
var res = Native.GetMessageByIdx(this.chatid, this.quoteIdx);
if (res === false) return; // Nothing to quote
this.quoteIdx = res[0];
this.processQuote(res[1], false, limitWidth);
}
}
}
processQuote(qs, insertQuote = true, limitWidth = false) {
this.input.state.focus = true;
var res = "";
var qslist = qs.split("\n");
for (let line of qslist) {
line = line.trimRight();
if (line == "") continue;
var leading = "";
for (var c = 0; c < line.length; ++c) {
var cc = line.charAt(c);
if (cc == ">" || cc == " ")
leading += cc;
else
break;
}
if (limitWidth) line = Native.WrapText(line, 50);
var list = line.split("\n");
for (let line2 of list) {
var t;
if (leading.length > 0 && leading[0] == ">")
t = ">" + line2;
else
t = "> " + line2;
res += t + "\n";
}
}
res += "\n";
var i = this.quoteIdx;
if (insertQuote) {
this.addToInput(res);
} else {
this.input.saveCaretPos();
if (this.lastInputText) res = this.lastInputText + "\n" + res;
this.input.value = res;
this.input.state.focus = true;
if (settings.cursorBelow)
this.input.execCommand("navigate:end");
else
this.input.restoreCaretPos();
this.changeInput();
}
this.post(() => this.quoteIdx = i);
}
updateAvatar() {
var avt = "avatar:" + this.chatid;
this.loadImage(avt, (image) => {
if (image !== null) {
document.bindImage(avt, image);
this.avatarimg.attr["src"] = "avatar:" + this.chatid;
this.avatar.classList.remove("hidden");
this.avatarSplit.state.disabled = false;
} else this.clearAvatar();
}, false);
}
clearAvatar() {
this.avatar.classList.add("hidden");
this.avatarimg.attr["src"] = undefined;
this.avatarSplit.state.disabled = true;
}
updateSmartReplies() {
this.smartReplies.clear();
var sreplies = lastSmartReplies[this.chatid];
if (!sreplies) return;
var replies = [];
for (let sreply of sreplies) replies.push(<div>{sreply}</div>)
this.smartReplies.append(replies);
}
}
class FileDropZone extends Element {
isDragActive = false;
textData = "";
shiftX = 26dip.valueOf();
shiftY = 18dip.valueOf();
moveToCursor(el) {
var [x, y] = view.box("position", "cursor");
el.takeOff({
x: x + this.shiftX, y: y + this.shiftY,
window: "popup"
});
}
hideDrags() {
dragHtml.state.expanded = false;
dragHtml.takeOff();
dragFile.state.expanded = false;
dragFile.takeOff();
chatButtons.fileBtn.state.expanded = false;
}
["on ^dragaccept"](e) {
if (e.detail?.dataType == "html") return false;
return true;
}
["on dragenter"](e) {
this.isDragActive = true;
if (e.detail?.dataType == "file") {
var hint = "";
if (Array.isArray(e.detail.data)) {
var cnt = 0;
for (let file of e.detail.data) cnt++;
hint = printf(_("Upload ZIP archive with %d files to %s"), cnt, CommonNative.GetUploadServerName());
} else if (typeof e.detail.data == "string") {
hint = printf(_("Upload %s to %s"), new URL(e.detail.data).filename, CommonNative.GetUploadServerName());
} else hint = _("Unknown");
dragFile.$("span").html = hint;
dragFile.$("> div[pic]").setupIcon();
this.moveToCursor(dragFile);
dragFile.state.expanded = true;
chatButtons.fileBtn.state.expanded = true;
} else {
this.textData = e.detail.data.toString();
dragHtml.$("span").text = this.textData;
this.moveToCursor(dragHtml);
dragHtml.state.expanded = true;
}
return true;
}
["on dragleave"](e) {
this.hideDrags();
return true;
}
["on drag"](e) {
this.moveToCursor(e.detail?.dataType == "file" ? dragFile : dragHtml);
return true;
}
["on drop"](e) {
this.isDragActive = false;
var page = getCurrentPage();
if (page) page.input.state.disabled = false;
if (e.detail?.dataType == "file") {
dragFile.state.expanded = false;
dragFile.takeOff();
chatButtons.fileBtn.state.expanded = false;
if (Array.isArray(e.detail.data)) {
var files = e.detail.data;
for (let [index, file] of files.entries())
files[index] = files[index].toLocalPath();
Native.UploadFiles(true, files.join(";"));
} else if (typeof e.detail.data == "string") {
Native.UploadFiles(false, e.detail.data.toLocalPath());
}
} else {
dragHtml.state.expanded = false;
dragHtml.takeOff();
var page = getCurrentPage();
if (page && page.pagetype !== "plugin")
page.addToInput(this.textData);
}
return true;
}
}
// DragDrop({
// what: "div#tabs > .tab",
// where: "div#tabs",
// container: "div#tabs",
// ignore: ".tab-close",
// easeDrop: Animation.Ease.OutQuad,
// acceptDrag: function(draggable, target) {
// draggable.chat.setCurrent();
// draggingTab = true;
// Native.SetTabDragging(true);
// return #moving;
// },
// onFinalize: function() {
// updateTabsDepth();
// draggingTab = false;
// Native.SetTabDragging(false);
// },
// animationDuration: 200ms,
// autoScroll: true
// });
function isSyntaxHighlightEnabled() {
return document.body.style.variable("syntax-highlight") === "on";
}
function setupHint(el) {
if (!el.attr["hint"]) return;
if (!el.attr["hint_en"]) el.attr["hint_en"] = el.attr["hint"];
el.attr["hint"] = _(el.attr["hint_en"] ? el.attr["hint_en"] : el.attr["hint"]);
el.on("mouseenter", (e) => setStatusBarHint(el.attr["hint"], el.attr["sticker"] !== undefined))
.on("mouseleave", (e) => setStatusBarHint(""));
}
function updateHintsTraslation() {
var hints = $$("[hint]");
for (let hint of hints) setupHint(hint);
}
function ShowHint() {
setupHint(this);
}
function isEventSelected(el) {
return el.classList.contains("selected");
}
function selectEvent(el) {
if (!el.classList.contains("selected"))
el.classList.add("selected");
}
function deselectEvent(el) {
if (el.classList.contains("selected"))
el.classList.remove("selected");
}
function showImage(imgTag, link) {
if (link.indexOf("embedded:") == 0) {
imgTag.attr["src"] = link;
finishImage(imgTag);
} else {
var cached = false;
try {
cached = document.bindImage("download:" + link) !== null;
} catch(e) {
cached = true;
}
imgTag.attr["src"] = "download:" + link;
if (imgTag.tag == "lottie") imgTag.lottie.load("download:" + link);
if (cached) {
imgTag.attr["cached"] = true;
finishImage(imgTag);
}
}
}
function finishImagesByLink(link) {
var imgTags = pages.$$("img[src=\"" + link + "\"], lottie[src=\"" + link + "\"]");
for (let imgTag of imgTags) finishImage(imgTag);
}
function finishImage(imgTag) {
if (!imgTag || imgTag.classList.contains("loaded")) return;
if (imgTag.attr["lottie"] == "true") {
var lottie = Element.create("lottie");
lottie.attr["class"] = imgTag.attr["class"];
lottie.attr["src"] = imgTag.attr["src"];
lottie.attr["autoplay"] = "";
lottie.attr["loop"] = "";
imgTag.parent.append(lottie);
imgTag.remove();
imgTag = lottie;
}
var pg = imgTag.$p(".page");
var bottomScroll = msgPreview ? false : scrollImageToBottom(pg, imgTag);
let parent = imgTag.parent;
if (parent) {
var aTag = parent.prior?.prior;
if (aTag) aTag.classList.remove("checking");
parent.classList.remove("hidden");
}
if (!imgTag.classList.contains("embeddedImg") && !imgTag.classList.contains("cached")) imgTag.flushPaint();
if (!msgPreview)
if (bottomScroll)
pg.scrollToBottom(false);
else if (pg.bottomPos >= 0)
pg.chatHist.scrollToEx(0, pg.chatHist.state.box("height", "content") - pg.chatHist.state.box("height", "client") - pg.bottomPos, false);
imgTag.classList.add("loaded");
}
class LoadedImage extends Element {
componentDidMount() {
//console.log("LoadedImage:", this.tag, this.attr["src"], this.attr["style"]);
this.post(() => this.attr["style"] = "");
this.on("mousedown", (e) => openOverlay(e, this));
}
}
function scrollImageToBottom(pg, imgTag) {
if (!pg) return false;
return pg.isAtBottom() && pg.chat.children.length > 0 && imgTag.$p(".msgFull") == pg.chat.last;
}
function getImg(imgTag) {
try {
var action = imgTag.attr["data-action"];
var link = imgTag.attr["data-link"];
imgTag.removeAttribute("data-action");
imgTag.removeAttribute("data-link");
if (action == "check") // not in cache, check first
fetch(new Request("check:" + link, { method: "GET", cache: "reload" })).then((resp) => resp.json()).then((data) => {
if (imgTag)
if (data && data.isImg === 1) { // image link confirmed, show preloader and start download
var pg = imgTag.$p(".page");
var bottomScroll = !msgPreview && scrollImageToBottom(pg, imgTag);
var aTag = imgTag.parent?.prior.prior;
if (aTag) aTag.classList.add("checking");
if (data.isLottie === 1) imgTag.attr["lottie"] = "true";
showImage(imgTag, data.link);
} else { // not an image link, remove loader
if (imgTag.parent) imgTag.parent.remove();
}
}, (e) => {
console.error(e, e.stack);
if (imgTag.parent) imgTag.parent.remove();
});
else { // image was already cached or embedded
showImage(imgTag, link);
}
} catch(e) {
console.error(e, e.stack);
}
}
function viewInWindow(uid, title, body, when, formicon) {
var msg = getChatPage(uid).$(".msgFull[time=\"" + when + "\"]");
var images = [];
var winid = "";
if (msg) {
var embeddedImgs = msg.$$("img[src^=\"embedded:\"]");
for (let embeddedImg of embeddedImgs)
images.push(embeddedImg.attr["src"]);
winid = msg.attr["msgid"];
} else {
winid = "selection";
}
openTextWindow(uid + ":" + winid, title, body, settings.viewTextWrap, images, formicon);
}
function getChatPage(uid) {
for (let page of pages.children)
if (page.chatid == uid) return page;
}
function getPluginPage(id) {
for (let page of pages.children)
if (page.plugid == id) return page;
}
function openChatPage(uid, displayed, focused) {
var page = Element.create(<Page pagetype="im" uid={uid} displayed={displayed} focused={focused} />);
pages.append(page);
page.init();
}
function openPluginPage(id, displayed) {
var page = Element.create(<Page pagetype="plugin" plugid={id} displayed={displayed} />);
pages.append(page);
page.init();
}
function getPluginBounds() {
var [x1, y1, x2, y2] = pages.state.box("rect", "border", "window");
var padding = 5dip.valueOf();
return [x1 + padding, y1 + padding, x2 - padding, y2 - padding];
}
function addPluginButton(id) {
if (id == 0) return;
var plugBtn = Element.create("div");
var plugBtnImg = Element.create("div");
plugBtn.attr["proc"] = id;
plugBtn.append(plugBtnImg);
plugButtons.append(plugBtn);
}
function modifyPluginButton(id, hint, pic) {
var plugBtn = plugButtons.$("div[proc=\"" + id + "\"]");
if (!plugBtn) return;
if (pic) plugBtn.$("> div").applySprite(getSpriteData(pic), "plugin");
plugBtn.attr["hint"] = hint;
}
function delPluginButton(id) {
var plugBtn = plugButtons.$("div[proc=\"" + id + "\"]");
if (plugBtn) plugBtn.remove();
}
function setCurrentPage(dir) {
var index = tabs.$(":checked").index;
index += dir;
if (index < 0)
index = tabs.length - 1;
else if (index >= tabs.length)
index = 0;
tabs.children[index].chat.setCurrent();
}
function initSettings(sets) {
settings = sets;
view.isTopmost = settings.alwaysOnTop;
settings.screenshotFormatSymbol = settings.screenshotFormat == 2 ? "webp" : (settings.screenshotFormat == 1 ? "jpeg" : "png");
switch (settings.imageQuality) {
case 0: pages.style["image-rendering"] = "optimize-quality"; break;
case 1: pages.style["image-rendering"] = "optimize-speed"; break;
case 2: pages.style["image-rendering"] = "default"; break;
}
var step = settings.wheelVelocity == 3 ? 10 : settings.wheelVelocity * 3.3;
pages.style["scroll-manner"] = "scroll-manner(animation: " + (settings.animatedScroll ? "true": "false") + ", wheel-step: " + step + "%)";
if (settings.showSmileCaption)
smilesList.children[0].classList.add("captions");
else
smilesList.children[0].classList.remove("captions");
setupRelTimesUpdates();
var spellErrorStyles = {
0: "solid",
1: "dotted",
2: "dashed",
3: "wavy"
};
var dyncss = $("style#dyncss");
dyncss.text = ".input > text::mark(wrongword) { text-decoration: underline " + spellErrorStyles[settings.spellErrorStyle] + " " + settings.spellErrorColor + " }";
dyncss.text += ".renderOption { font-rendering-mode: " + (settings.smoothFontRendering ? "sub-pixel" : "snap-pixel") + " }";
dyncss.text += ".avatar { display: " + (settings.showAvatar ? "block" : "none") + " }";
dyncss.text += ".smartReplies { display: " + (settings.showSmartReplies ? "block" : "none") + " }";
dyncss.text += ".msgReaction { display: " + (settings.showReactions ? "inline-block" : "none") + " }";
dyncss.text += ".msgTitle .msgReactBtn { display: " + (settings.showReactions ? "block" : "none") + " }";
dyncss.text += ".input > text::mark(wrongword) { text-decoration: underline " + spellErrorStyles[settings.spellErrorStyle] + " " + settings.spellErrorColor + "; }";
function cssStyleMax() {
return "max-width: " + (settings.maxImgWidth > 0 ? settings.maxImgWidth + "px" : "calc(100vw - 41dip)") + "; " +
"max-height: " + (settings.maxImgHeight > 0 ? settings.maxImgHeight + "px" : "none") + ";";
}
// function cssStyleLottie() {
// return "width: " + (settings.maxImgWidth > 0 ? settings.maxImgWidth : 256) + "px; " +
// "height: " + (settings.maxImgHeight > 0 ? settings.maxImgHeight : 256) + "px;";
// }
dyncss.text += ".msgBody .linkedImg { " + cssStyleMax() + " }";
dyncss.text += ".msgBody lottie.linkedImg { " + cssStyleMax() + " }";
dyncss.text += ".msgBody .embeddedImg { " + cssStyleMax() + " }";
dyncss.text += "span.bold { font-weight: " + (settings.fontCodes ? 500 : "normal") + " }";
dyncss.text += "span.underlined { text-decoration: " + (settings.fontCodes ? "underline" : "none") + " }";
var usercss = $("style#usercss");
usercss.text = settings.chatCSS;
}
function initMsgPreview(searchText) {
msgPreview = true;
document.body.classList.add("msgPreview");
if (!$(".searchHere"))
document.body.append(<div class="searchHere">{searchText}</div>);
}
function applyTheme() {
reloadImage("themepicsingle:msg");
for (let el of $$("menu.custom li, button.ddbutton, #chatButtons > *, #sbOutbox, #myreactions > div")) {
var icon = el.$("> div[pic]");
if (icon) icon.setupIcon();
}
if (initialLoad) {
initialLoad = false;
return;
}
var bimages = $$("[style], div[pic], .chatBackground, .chatHist");
var themepics = [];
for (let bimage of bimages)
if (bimage) {
var link = bimage.style["background-image"];
if (!link || link.indexOf("themepic:") === -1 || themepics.indexOf(link) !== -1) continue;
themepics.push(link);
}
for (let themepic of themepics) reloadImage(themepic);
for (let page of pages.children) page.resetHistory();
themepics = [];
}
function showSearchHere() {
if (getCurrentPage().chat.children.length == 0)
$(".searchHere").classList.remove("hidden")
}
function hideSearchHere() {
$(".searchHere").classList.add("hidden")
}
function reloadSmiles(smiles) {
if (smilesGrid.parent == null) document.body.append(smilesGrid);
smilesList.list.clear();
if (smiles)
for (let sm of smiles) if (sm) {
reloadImage("smile:n" + sm[0]);
smilesList.list.append([<div class="sesbtn" text={sm[1]} hint={sm[1]}><div style={"background-image: url(smile:n" + sm[0] + "); width: " + sm[2] + "px; height: " + sm[3] + "px;"}></div></div>, <div class="caption">{sm[1]}</div>]);
}
if (smilesList.list.length > 0 && smilesList.list.first?.state) smilesList.list.first.state.current = true;
smilesGrid.detach();
}
function makeEmojiBtn(em, emojiSize) {
return <div style={"background-image: url(themepic:emojis.sprite); background-position: " + em[1] + " " + em[2] + "; width: " + emojiSize + "px; height: " + emojiSize + "px"}></div>;
}
function makeEmojiCat(cat, emojiNums, emojiSize) {
return <div class="catbtn" hint={emojiNums[cat][0]} category={cat}>{makeEmojiBtn(emojiNums[cat], emojiSize)}</div>;
}
function reloadEmojis(hasEmojis, emojiSize, emoji, emojiNums) {
emojiList = $("#emojiList");
reloadImage("themepic:emojis.sprite");
emojiPages.length = 0;
if (!hasEmojis) {
emojiCat.clear();
emojiList.clear();
return;
}
var cats = [];
if (emoji)
for (let [cat, emojiCat] of emoji.entries()) {
if (!emojiCat) continue;
cats.push(makeEmojiCat(cat, emojiNums, emojiSize));
var emlist = [];
for (let em of emojiCat)
emlist.push(<div class="sesbtn" text={em[0]}>{makeEmojiBtn(em, emojiSize)}</div>);
emojiList.content(<div class="emlist">{emlist}</div>);
emojiPages[cat] = emojiList.children[0];
emojiPages[cat].detach();
}
emojiCat.content(cats);
if (emojiCat.length > 0) emojiCat.children[0].state.checked = true;
emojiList.clear();
if (emojiPages.length > 0) {
emojiList.append(emojiPages[0]);
emojiList.initList();
}
}
function preloadImages(urlList) {
if (urlList === "") return;
var urls = urlList.split("\n");
for (let url of urls) reloadImage(url.trim(), true);
}
function delphi2unix(time) {
return (time - 25569.0) * 86400000.0;
}
function unix2delphi(time) {
return time / 86400000.0 + 25569.0;
}
function getCurrentPage() {
return pages.$("> .page:current");
}
function setSendBtnImage(pic) {
sendBtnImg.applySprite(pic);
}
function sendMessageToCurrent(opt = 0) {
var page = getCurrentPage();
if (page) page.sendMessage(opt);
}
function closePages(opt = 0) {
closeVariant.value = undefined;
document.post(() => Native.ClosePages(opt));
}
function closeForm() {
var popups = $$(":popup");
for (let popup of popups) popup.state.popup = false;
backTabs.clear();
hide();
}
function uploadFile(opt = 0) {
var url = Native.UploadFile(opt);
if (url) {
var page = getCurrentPage();
if (!page || page.pagetype == "plugin") return;
page.addToInput(url);
}
}
function selectContactsToSend() {
var UINs = [];
UINs = openUINList("Send contacts", "Select", ["sco_multi", "sco_groups", "sco_predefined"]);
if (!UINs || UINs.length == 0) return;
var page = getCurrentPage();
for (let UIN of UINs) page.addToInput("https://icq.im/" + UIN + "\n");
}
function openSearch(searchAll = true) {
var page = getCurrentPage();
if (searchAll || page.pagetype != "im")
openHistorySearch();
else
openHistorySearch(page.chatid, page.displayed);
}
function openInfo() {
Native.ChatButtonClick("infoBtn");
}
function openSmiles() {
if (smilesList.list.length == 0) {
showAlert("warning", "No smiles found in current theme");
return;
}
document.body.append(smilesGrid);
chatButtons.smilesBtn.parent.popup(smilesGrid, {
anchorAt: 7,
popupAt: 1
});
smilesList.list.state.focus = true;
}
function openEmoji() {
if (emojiList.length == 0) {
showAlert("warning", "Emoji image cannot be found in current theme");
return;
}
chatButtons.emojiBtn.parent.popup(emojiGrid, {
anchorAt: 7,
popupAt: 1
});
emojiList.list.state.focus = true;
}
function searchStickers() {
var emlist = $(".imagegrid#stickersGrid .imagelist div.emlist[category=\"-1\"]");
if (!emlist) return;
emlist.clear();
if (sKeywords == "") return;
var found = Native.SearchStickersByKeywords(sKeywords);
if (!found || found.length == 0)
emlist.append(<div class="nothingfound">{_("Nothing can be found")}</div>);
else for (let st of found)
emlist.append(<div class="sesbtn" cat="-1" sticker={st[1]} hint={Native.SpanEmojis(st[2])}>{makeStickerBtn(st[1], false, st[0] == "animated")}</div>);
}
function switchToStickerCat(cat) {
stickersList.content(stickersPages[cat]);
stickersList.initList();
stickersList.scrollTo({
position: [0, 0],
behavior: "instant"
});
var stsearch = stickersList.$("#stsearch");
if (stsearch) {
stsearch.value = sKeywords;
stsearch.edit.selectRange(sKeywords.length, sKeywords.length);
stsearch.state.focus = true;
searchStickers();
} else {
stickersList.list.state.focus = true;
}
}
function openStickers() {
var keys = Object.keys(stickersPages);
if (keys.length == 1) {
openStickersManager();
return;
}
var page = getCurrentPage();
if (page && page.pagetype == "im")
if (page.input.selection.toString() !== "") sKeywords = page.input.selection.toString();
chatButtons.stickersBtn.parent.popup(stickersGrid, {
anchorAt: 7,
popupAt: 1,
});
if (stickersList.length == 0) {
var cat = stickersCat.$(".catbtn:checked");
switchToStickerCat(cat ? cat.attr["category"] : keys[0]);
}
}
function openStickersManager() {
var [px, py, dw, dh] = view.screenBox("frame", "rectw");
sMgr = openSingleWindow({
url: "stickersmgr.htm",
caption: "Stickers manager",
uniqueid: "stickersmgr",
alignment: 5,
width: dw / 3,
height: dh / 2.5
});
sMgr.on("close", function(e) {
sMgr.off("close");
sMgr = null;
});
}
function updateStickersManager() {
if (sMgr !== null) sMgr.updateMyStickerPacks();
}
function updateSearchResults(res) {
if (sMgr !== null) sMgr.updateSearchResults(res);
}
function startStickersSearch(qry) {
if (sMgr !== null) sMgr.startStickersSearch(qry);
}
function makeQuote(fromClipboard = false, limitWidth = false) {
getCurrentPage().quote(fromClipboard ? Clipboard.readText() : "", limitWidth);
}
function setupStickersBtn(st) {
chatButtons.stickersBtn.state.disabled = !st;
}
function setupBuzzBtn(st) {
chatButtons.buzzBtn.setVisible(st);
}
function setupSingleBtn(st) {
chatButtons.singleBtn.state.checked = st;
}
function setupFileBtn(st) {
chatButtons.fileBtn.state.disabled = !st;
}
function setupChatButtons(st) {
for (let chatBtn of chatButtons.self.children) chatBtn.state.disabled = st;
// for (let plugBtn of plugButtons) plugBtn.state.disabled = st;
}
function updateStatusBar(outbox, trlt, encpic, enchint) {
var pic = $("#sbOutbox > div");
pic.applySprite(getSpriteData(outbox ? "outbox" : "outbox.empty"));
pic.style.opacity = outbox ? 1 : 0.5;
$("#sbTrlt > div").style.opacity = trlt ? 0.8 : 0.4;
var pic = $("#sbCrypt > div#pic");
if (encpic) {
pic.setVisible(true);
pic.applySprite(getSpriteData(encpic));
} else pic.setVisible(false);
$("#sbCrypt").attr["hint"] = enchint;
}
function setStatusBarHint(hint, html = false) {
if (html)
$("#sbHint").html = hint;
else
$("#sbHint").text = hint;
}
function setSmartReplies(chat, suggests) {
lastSmartReplies[chat] = suggests;
var page = getChatPage(chat);
if (page) page.updateSmartReplies();
}
function makeStickerBtn(id, isPicker, isLottie) {
if (isLottie && !isPicker)
return <lottie src={"sticker:a" + id} autoplay loop />;
else
return <img src={(isPicker ? (isLottie ? "picker:a" : "picker:n") : "sticker:n") + id} />;
}
function loadStickers(data) {
updateStickersManager();
stickersPages = {};
var cats = [];
cats.push(<div class="catbtn" hint={_("Search")} category="-1"><div pic="search_button2" auto></div></div>);
stickersPages["-1"] = [<div class="stinputdiv"><input id="stsearch" class="stinput" /></div>, <div class="emlist" category="-1"></div>];
if (data)
for (let pack of data) {
if (!pack.Content || pack.Content.length == 0) continue;
var isLottie = pack.ContentType == "animated";
cats.push(<div class="catbtn" hint={pack.Name} category={pack.Id}>{makeStickerBtn(pack.Content[0], true, isLottie)}</div>);
var emlist = [];
var pos = 0;
for (let st of pack.Content) {
var hint = "";
if (pack.Keywords && pos < pack.Keywords.length) hint = Native.SpanEmojis(pack.Keywords[pos]);
emlist.push(<div class="sesbtn" cat={pack.Id} sticker={st} hint={hint}>{makeStickerBtn(st, false, isLottie)}</div>);
pos++;
}
stickersPages[pack.Id.toString()] = <div class="emlist" category={pack.Id}>{emlist}</div>;
}
stickersCat.content(cats);
if (stickersCat.length > 1) stickersCat.children[1].state.checked = true;
else if (stickersCat.length > 0) stickersCat.children[0].state.checked = true;
stickersList.clear();
}
function show() {
if (msgPreview) return;
if (view.state == View.WINDOW_SHOWN) return;
view.loadIcon("themepicsingle:msg");
view.state = View.WINDOW_SHOWN;
Native.OnChatShow();
}
function hide() {
if (msgPreview) return;
if (view.state == View.WINDOW_HIDDEN) return;
if (view.state != View.WINDOW_MAXIMIZED && view.state != View.WINDOW_FULL_SCREEN)
Native.UpdateChatXY(getBounds());
Native.OnChatHide();
view.state = View.WINDOW_HIDDEN;
}
function updateCaption(caption) {
if (msgPreview) return;
view.caption = caption;
}
function updateTranslation() {
translateWindow("menu.custom li > span, menu.context li > label, menu.custom hr > span, menu.custom caption, button > span, .indicator");
}
document.on("ready", () => {
updateTranslation();
}).on("closerequest", (e) => {
e.preventDefault();
hide();
});
function relTimesTimerFunc() {
if (settings.showRelTimes) updateRelTimes();
return settings.showRelTimes;
}
function setupRelTimesUpdates() {
if (settings.showRelTimes)
document.timer(20s, relTimesTimerFunc);
else
document.timer(0, relTimesTimerFunc);
}
function toggleRelTimes() {
settings.showRelTimes = !settings.showRelTimes;
Native.ToggleRelTimes();
updateRelTimes();
setupRelTimesUpdates();
}
function updateRelTimes() {
for (let page of pages.children)
for (let msg of page.chat.children) if (msg)
msg.$(".msgDate").text = Native.GetEventHeaderTime(parseFloat(msg.attr["time"]) || 0);
}
histmenu["self"] = $("#histmenu");
for (let menuItem of histmenu.self.$$("li, hr"))
histmenu[menuItem.id] = menuItem;
histmenu.self.on("click", function(e) {
this.ondismiss = () => {
rightClickedTime = "0";
rightClickedData = null;
};
var tgt = e.target;
//if (tgt.attr["parentmenu"] == "hm_contactmenu")
//Native.AddUIN2CL(rightClickedData.substr(4), tgt.attr["tag"]);
var owner = this.$o(":owns-popup");
switch (tgt.id) {
case "hm_contactmenu": {
var [x, y] = this.state.box("position", "margin", "window");
var uid = rightClickedData.substr(4);
document.post(() => openContactMenu(uid, x, y));
break;
}
case "hm_copylink": Native.CopyLink(rightClickedData); break;
case "hm_copy": owner.copySelected(); break;
case "hm_savepic": Native.SavePicture(rightClickedData); break;
case "hm_stickerpack": {
if (!CommonNative.CheckOnline()) return;
var fileid = rightClickedData.substr(rightClickedData.toLowerCase().indexOf(icqfilelnk) + icqfilelnk.length);
document.timer(100ms, () => {
openStickersManager();
startStickersSearch("fileid:" + fileid);
});
break;
}
case "hm_selectall": owner.selectAll(); break;
case "hm_resethist": owner.resetHistory(); break;
case "hm_viewinwin":
if (owner.selInfo.text != "") {
viewInWindow(owner.chatid, _("Selected"), owner.selInfo.text, "", "");
} else if (rightClickedTime != "0") {
var ev = Native.GetEvent(owner.chatid, rightClickedTime);
if (ev)
viewInWindow(owner.chatid, ev.caption, ev.text, ev.when, ev.img);
else
showAlert("warning", "This event was not saved in history");
}
break;
case "hm_saveastxt": Native.SaveAs(0); break;
case "hm_saveashtml": Native.SaveAs(1); break;
case "hm_addtofav": Native.AddLinkToFav(rightClickedData); break;
case "hm_edit": {
var chat = owner.chatid;
var data = rightClickedData;
var text = Native.GetChatMessageText(chat, data);
if (text === undefined) return;
var edited = openTextEdit(text, true);
if (edited !== null && edited !== "" && edited !== text)
Native.EditChatMessage(chat, data, edited);
break;
}
case "hm_delete": {
var option = createDialog(
"question",
_("Delete selected messages"),
_("Choose where messages should be deleted from"),
false, [
["forme", _("Server (for self only)")],
["forall", _("Server (for everyone)"), true],
["local", _("Local history")],
"mbCancel"
], "mbCancel", "mbCancel"
);
if (option === "mbCancel") return;
var msgids = [];
if (option === "forme" || option === "forall") {
var missing = false;
var nodes = owner.chat.$$("> .msgFull.selected");
if (nodes.length > 0)
for (let node of nodes)
if (node.attr["msgid"] !== "" && (parseFloat(node.attr["msgid"]) || 0) > 1000) // don't send ack IDs
msgids.push(node.attr["msgid"]);
else
missing = true;
if (missing && createDialog(
"asterisk",
_("Delete selected messages"),
_("Some messages are missing unique ID and cannot be removed from server. Proceed?"),
false, ["mbYes", "mbNo"], "mbYes", "mbNo"
) === "mbNo") return;
}
Native.DeleteMessages(owner.chatid, option, msgids);
break;
}
case "hm_reactions": Native.GetReactions(owner.chatid, rightClickedData); break;
case "hm_antispam": Native.AddToAntispam(); break;
case "hm_openwith": openChatWith(); break;
case "hm_viewinfo": openViewInfo(owner.chatid, owner.displayed); break;
case "hm_imgsmiles": Native.ToggleSmiles(); break;
case "hm_reltimes": toggleRelTimes(); break;
}
});
codemenu.on("click", function(e) {
function insertDecor(input, decor) {
var sel = input.selection.toString();
insertText(input, decor + sel + decor);
if (sel == "") input.execCommand("navigate:backward");
input.state.focus = true;
}
function insertCode(input, lang = "") {
if (lang == "")
insertText(input, "[code]\n" + input.selection.toString() + "\n[/code]");
else
insertText(input, "[code=" + lang + "]\n" + input.selection.toString() + "\n[/code]");
input.execCommand("navigate:line-start");
input.execCommand("navigate:backward");
input.state.focus = true;
}
var owner = this.$o(":owns-popup");
switch (e.target.id) {
case "cm_bold": insertDecor(owner, "*"); break;
case "cm_underline": insertDecor(owner, "_"); break;
case "cm_syntax_generic": insertCode(owner); break;
case "cm_syntax_html": insertCode(owner, "html"); break;
case "cm_syntax_js": insertCode(owner, "js"); break;
case "cm_syntax_css": insertCode(owner, "css"); break;
case "cm_syntax_json": insertCode(owner, "json"); break;
case "cm_syntax_php": insertCode(owner, "php"); break;
case "cm_syntax_delphi": insertCode(owner, "delphi"); break;
case "cm_syntax_c": insertCode(owner, "c"); break;
}
});
sendBtn.on("click", () => sendMessageToCurrent());
sendVariant.on("click", function(e) {
var [x, y, w, h] = this.parent.state.box("rectw", "border", "window");
var onedip = 1dip.valueOf();
this.parent.popup(sendmenu, {
anchorAt: 7,
x: x - onedip,
y: y + h - onedip
});
});
sendmenu.on("click", (e) => {
sendMessageToCurrent(parseInt(e.target.value));
});
closeBtn.on("click", () => closePages());
closeVariant.on("click", function(e) {
var [x, y, w, h] = this.parent.state.box("rectw", "border", "window");
var onedip = 1dip.valueOf();
this.parent.popup(closemenu, {
anchorAt: 7,
x: x - onedip,
y: y + h - onedip
});
});
closemenu.on("click", (e) => {
closePages(parseInt(e.target.value));
});
chatButtons["self"] = $("#chatButtons");
for (let btn of chatButtons.self.$$("> div > div"))
chatButtons[btn.id] = btn.parent;
chatButtons.self.on("^mouseup", function(e) {
var btn = e.target.id ? e.target.parent : e.target;
var pic = e.target.id ? e.target : e.target.$("> div");
if (!btn || !pic) return;
var [x, y] = btn.state.box("position", "border", "desktop");
x -= 3; y -= 6;
var st = e.propButton;
if (pic.id == "smilesBtn") { openSmiles(); return; }
else if (pic.id == "emojiBtn") { openEmoji(); return; }
else if (pic.id == "stickersBtn") {
if (e.propButton)
openStickersManager();
else
openStickers();
return;
} else if (pic.id == "searchBtn") {
openSearch(e.propButton);
return;
} else if (pic.id == "prefBtn") {
openPrefs(e.propButton ? "Plugins" : "Chat", false);
return;
} else if (pic.id == "contactsBtn") {
selectContactsToSend();
return;
} else if (pic.id == "quoteBtn") {
makeQuote(st, e.ctrlKey);
return;
} else if (pic.id == "fileBtn" && st) {
openPrefs("Other", false);
return;
}
if (pic.id == "singleBtn") st = !chatButtons.singleBtn.state.checked;
Native.ChatButtonClick(pic.id, st, x, y);
});
chatButtons.fileBtn.on("click", function(e) {
this.popup(filemenu, {
anchorAt: 1,
popupAt: 7
});
});
filemenu.on("click", (e) => {
uploadFile(parseInt(e.target.value));
});
var ax, ay = 0;
var cx, cy = 0;
var draggingSnap = false;
const DRAG_THRESHOLD = 3;
var monitors = [];
var snapshots = [];
function requestSnapshotAction() {
var result = createDialog(
"question",
_("Action required"),
_("Choose what to do with the screenshot"),
false, [
["upload", _("Upload to server")],
["save", _("Save image to file")],
"mbCancel"
], "upload", "mbCancel"
);
if (result == "upload")
Native.UploadLastSnapshot();
else if (result == "save")
Native.SaveAs(2);
else
Native.DeleteSnapshot();
}
function captureRegion() {
var [sx, sy, ex, ey] = [Math.min(ax, cx), Math.min(ay, cy), Math.max(ax, cx), Math.max(ay, cy)];
var cropWidth = ex - sx + 1;
var cropHeight = ey - sy + 1;
if (cropWidth <= 1 && cropHeight <= 1) return;
document.timer(500ms, () => {
for (var m = 0; m < View.screens; ++m)
snapshots.push(View.screenBox(m, "snapshot"));
var img = new Graphics.Image(cropWidth, cropHeight, function(gfx) {
for (var m = 0; m < View.screens; ++m)
gfx.draw(snapshots[m], {
x: monitors[m].x - sx,
y: monitors[m].y - sy,
srcX: 0,
srcY: 0,
srcWidth: monitors[m].w,
srcHeight: monitors[m].h
});
});
img.toBytes(settings.screenshotFormatSymbol, 95).save(settings.cachePath + settings.screenshotFilename);
snapshots = [];
requestSnapshotAction();
});
}
function closeSnapLayer() {
if (snapLayer == null) return false;
draggingSnap = false;
snapLayer.state.capture(false);
snapLayer.paintForeground = null;
snapLayer.off(".tracker");
snapLayer.remove();
snapLayer = null;
return true;
}
function snapLayerMouseMove(e) {
if (!draggingSnap) {
if (Math.abs(ax - e.x) > DRAG_THRESHOLD || Math.abs(ay - e.y) > DRAG_THRESHOLD) {
draggingSnap = true;
cx = e.x;
cy = e.y;
snapLayer.paintForeground = (gfx) => {
gfx.strokeWidth = 1dip;
gfx.strokeStyle = Color.RGB(127, 127, 127);
gfx.fillStyle = Color.RGB(127, 127, 127, 127);
gfx.translate(0.5, 0.5)
.fillRect(ax, ay, cx - ax + 1, cy - ay + 1)
.strokeRect(ax, ay, cx - ax + 1, cy - ay + 1);
};
snapLayer.state.capture(true);
snapLayer.requestPaint();
return true;
}
return false;
} else {
cx = e.x;
cy = e.y;
snapLayer.requestPaint();
return true;
}
}
chatButtons.snapshotBtn.on("click", function(e) {
this.popup(snapmenu, {
anchorAt: 1,
popupAt: 7
});
});
snapmenu.on("click", function(e) {
var val = parseInt(e.target.value);
var screen = val % 10 - 1;
var action = Math.floor(val / 10);
// Entire screen
if (action == 1) {
document.timer(500ms, () => {
var img = View.screenBox(screen, "snapshot");
img.toBytes(settings.screenshotFormatSymbol, 95).save(settings.cachePath + settings.screenshotFilename);
requestSnapshotAction();
});
return;
}
// Selected region
if (!snapLayer) {
snapLayer = Element.create("div");
snapLayer.classList.add("snaplayer");
snapLayer.on("^mousedown.tracker", function(e) {
if (e.mainButton) {
ax = e.x;
ay = e.y;
cx = ax;
cy = ay;
view.focus = this;
this.on("mousemove.tracker", snapLayerMouseMove);
this.on("mouseup.tracker", () => {
captureRegion();
closeSnapLayer();
});
return true;
}
});
document.body.append(snapLayer);
}
var minx = 0,
miny = 0,
maxx = 0,
maxy = 0;
monitors = [];
for (var m = 0; m < View.screens; ++m ) {
var [x, y, w, h] = View.screenBox(m, "frame", "rectw");
minx = Math.min(minx, x);
miny = Math.min(miny, y);
maxx = Math.max(maxx, x + w);
maxy = Math.max(maxy, y + h);
monitors.push({ x: x, y: y, w: w, h: h });
}
snapLayer.takeOff({
x: minx, y: miny,
width: maxx - minx, height: maxy - miny,
relativeTo : "screen",
window: "popup"
});
snapLayer.state.focusable = true;
snapLayer.state.focus = true;
}).on("currentstatechange", function(e) {
if (e.target.parent && e.target.parent === subscreens) {
var modId = parseInt(e.target.value) - 11;
var [x, y, w, h] = View.screenBox(modId, "frame", "rectw");
monIndicator.style.display = "none";
monIndicator.takeOff();
monIndicator.content(<div>{modId + 1}</div>);
monIndicator.style.display = "block";
monIndicator.takeOff({
x: x + w - 50 - monIndicator.state.box("width", "border"), y: y + 40,
relativeTo : "screen",
window: "popup"
});
} else if (monIndicator.style.display != "none") {
monIndicator.style.display = "none";
monIndicator.takeOff();
}
});
subscreens.on("popupdismissing", function(e) {
monIndicator.style.display = "none";
monIndicator.takeOff();
});
if (View.screens > 1) {
for (var i = 1; i <= View.screens; i++)
subscreens.append(<li value={parseInt(subscreens.parent.value) + i}><span>{i}</span></li>);
} else {
subscreens.remove();
}
plugButtons.on("mouseup", "div[proc]", (e, el) => {
Native.PluginButtonClick(parseInt(el.attr["proc"]), el.attr["title"], e.buttons);
});
$("#sbOutbox").on("mouseup", () => openOutbox());
$("#sbTrlt").on("dblclick", (e) => { if (e.mainButton) Native.ToggleTranslit() });
var smilesGrid = $("#smilesGrid"),
smilesList = $("#smilesList");
emojiGrid = $("#emojiGrid"),
emojiCat = $("#emojiCat"),
emojiList = $("#emojiList"),
emojiPages = [],
stickersGrid = $("#stickersGrid"),
stickersCat = $("#stickersCat"),
stickersList = $("#stickersList"),
stickersPages = {};
emojiCat.on("click", ".catbtn", (e, el) => {
emojiList.list.detach();
emojiList.append(emojiPages[parseInt(el.attr["category"])]);
emojiList.initList(true);
});
stickersCat.on("click", ".catbtn", (e, el) => {
switchToStickerCat(el.attr["category"]);
});
stickersCat.on("keydown", ".catbtn", function(e) {
return true;
});
stickersCat.on("wheel", function(e) {
var dir = e.deltaY > 0 ? 1 : e.deltaY < 0 ? -1 : 0;
var posX = parseInt(stickersCat.scrollLeft + dir * settings.wheelVelocity * 48);
posX = Math.min(Math.max(0, posX), stickersCat.scrollWidth - stickersCat.state.box("width", "padding"));
stickersCat.scrollTo({
position: [posX, 0],
behavior: "smooth"
});
return true;
});
stickersList.on("change", "#stsearch", (e, el) => {
sKeywords = el.value;
if (sKeywords == "")
searchStickers();
else
document.timer(1s, searchStickers);
});
document.body.on("click", ".imagegrid .sesbtn, .imagegrid .caption", (e, el) => {
var el2 = el.attr["class"] == "caption" ? el.prior : el;
if (el.$p("#stickersList"))
Native.SendStickerToCurrent(el2.attr["sticker"]);
else
getCurrentPage().addToInput(el2.attr["text"]);
el.$p(".imagegrid").state.popup = false;
});
function overlayAction(keyCode) {
if (overlay && overlay.state.visible)
switch (keyCode) {
case Event.VK_RIGHT:
case Event.VK_UP:
case Event.VK_NEXT: {
if (overlay.length == 1) return closePreview();
var curr = overlay.$("img.visible");
curr.classList.remove("visible");
if (curr.next)
curr.next.classList.add("visible");
else
curr.parent.first.classList.add("visible");
return true;
}
case Event.VK_LEFT:
case Event.VK_DOWN:
case Event.VK_PRIOR: {
if (overlay.length == 1) return closePreview();
var curr = overlay.$("img.visible");
curr.classList.remove("visible");
if (curr.prior)
curr.prior.classList.add("visible");
else
curr.parent.last.classList.add("visible");
return true;
}
default:
return closePreview();
};
}
document.body.on("^keyup", (e) => overlayAction(e.keyCode));
tabs.on("wheel", function(e) {
var dir = e.deltaY < 0 ? 1 : e.deltaY > 0 ? -1 : 0;
var scrollPosX = parseInt(tabs.scrollLeft - dir * settings.wheelVelocity * tabs.state.box("width") / 20)
scrollPosX = Math.min(Math.max(0, scrollPosX), tabs.scrollWidth - tabs.clientWidth);
tabs.scrollTo({
position: [scrollPosX, 0],
behavior: "smooth"
});
return true;
});
pages.on("^mousedown", ".linkedImgWrapper, .msgEmbeddedImages", (e, el) => {
var page = el.$p(".page");
if (page != null) {
page.clearSelection();
page.saveSelection();
}
return !(e.target && e.target.tag == "img" && e.target.classList.contains("loaded"));
});
function openOverlay(e, el) {
if (!e.mainButton || !e.isOnIcon) return false;
var imgLinks = [];
var msg = el.$p(".msgFull");
if (msg) {
var linkedImgs = msg.$$("img[src^=\"download:\"]");
for (let linkedImg of linkedImgs)
imgLinks.push(linkedImg.attr["src"]);
var embeddedImgs = msg.$$("img[src^=\"embedded:\"]");
for (let embeddedImg of embeddedImgs)
imgLinks.push(embeddedImg.attr["src"]);
} else {
imgLinks.push(el.attr["src"]);
}
if (!overlay) {
overlay = Element.create(<div class="imgoverlay"></div>);
overlay.on("^mousedown", closePreview);
overlay.on("wheel", function(e) {
if (!this.classList.contains("active")) return false;
return overlayAction(e.deltaY > 0 ? Event.VK_UP : Event.VK_DOWN);
});
document.body.append(overlay);
for (let imgLink of imgLinks)
overlay.append(<img src={imgLink} class={imgLink == el.attr["src"] ? "visible" : ""} />);
}
var [sx, sy, sw, sh] = view.screenBox("frame", "rectw");
overlay.takeOff({
x: sx, y: sy,
width: sw, height: sh,
relativeTo : "screen",
window: "popup"
});
overlay.classList.add("active");
overlay.state.focusable = true;
overlay.state.focus = true;
return true;
}
pages.on("mousedown", ".avatar > img", openOverlay);
pages.on("click", "a[kind]", (e, el) => {
if (el.attr["kind"] == "other" && el.prior) {
linkinfo.first.clear();
linkinfo.last.clear();
linkinfo.first.append(<div class="row"><span class="spinner"><img src="resource:spinner" /></span></div>);
var [x, y, w, h] = el.state.box("rectw", "margin", "window");
el.popup(linkinfo, {
anchorAt: 7,
x: x + 6dip.valueOf(),
y: y + h + 20dip.valueOf()
});
var infos = Native.GetLinkInfo(el.attr["src"]);
linkinfo.first.clear();
for (let info of infos)
if (info.param == "AVATAR") {
linkinfo.last.append(<img src={info.value} />);
} else if (info.param == "TRANSCRIPTION") {
linkinfo.first.append(<div class="row"><span colspan="2" class="param separated">{_("Transcription")}</span></div>);
linkinfo.first.append(<div class="row"><span colspan="2" state-html={info.value}></span></div>);
} else {
linkinfo.first.append(<div class="row"><span class="param" colspan={info.value == "" ? 2 : 1}>{info.param}</span><span state-html={info.value}></span></div>);
}
linkinfo.state.disabled = true;
return;
}
var page = getCurrentPage();
if (videoview) {
videoview.detach();
page.chatBackground.insert(videoview);
} else {
videoview = Element.create(<VideoPreview />);
page.chatBackground.insert(videoview);
videoview.initChildren();
}
videoview.setLoadingText(_("Retrieving list of available video formats..."));
videoview.show();
videoview.page = page;
videoview.src = el.attr["src"];
videoview.kind = el.attr["kind"];
videoview.init = false;
videoview.timer(300ms, () => {
var links;
if (videoview.kind == "youtube")
links = Native.GetYoutubeLinks(videoview.src);
else if (videoview.kind == "vimeo")
links = Native.GetVimeoLinks(videoview.src);
if (!links || links.length == 0) {
videoview.error();
return;
}
if (links.length == 1) {
var linkdata = JSON.parse(links[0]);
videoview.title = linkdata.title;
videoview.startVideo(linkdata.url, linkdata.format, linkdata.codecs);
return;
}
var htmldata = [];
for (let link of links) {
var linkdata = JSON.parse(link);
videoview.title = linkdata.title;
htmldata.push(<div format={linkdata.format} codecs={linkdata.codecs} url={linkdata.url}>{linkdata.format}</div>);
}
var data = [];
data.push(_("Choose video format"));
data.push(<div id="videoformats">{htmldata}</div>);
data.push(<a href="#" close="">{_("Close")}</a>);
videoview.setLoadingText(data, true);
});
});
myreactions.on("click", function(e) {
if (!e.target) return;
this.state.popup = false;
var el = e.target.attr["option"] ? e.target : e.target.$p("div[option]");
if (el)
if (el.classList.contains("my"))
Native.RemoveReaction(this.attr["chatid"], this.attr["msgid"]);
else
Native.AddReaction(this.attr["chatid"], this.attr["msgid"], el.attr["option"]);
});
var isModKey = false;
document.on("^mouseup", (e) => {
if (e.buttons == 8) // browser backward
setCurrentPage(-1);
else if (e.buttons == 16) // browser forward
setCurrentPage(+1);
}).on("keydown", (e) => {
if (snapLayer) { closeSnapLayer(); return; }
if (e.keyCode == Event.VK_ESCAPE && !document.body.isDragActive) {
if (overlay && overlay.state.visible)
closePreview();
else
closeForm();
} else if (e.keyCode == Event.VK_C && e.ctrlKey) {
var page = getCurrentPage();
if (page && page.pagetype == "im") page.copySelected();
return true;
}
}).on("^keydown", function(e) {
isModKey = e.ctrlKey || e.shiftKey;
if (e.keyCode == Event.VK_CONTEXT_MENU) {
var page = getCurrentPage();
if (page && page.pagetype == "im")
if (!page.input.state.focus && $$(":popup").length == 0)
openContactMenu(page.chatid);
}
else if ((e.keyCode == Event.VK_W || e.keyCode == Event.VK_F4) && e.ctrlKey) closePages();
else if (e.keyCode == 170 /* VK_BROWSER_SEARCH */ ||
(e.keyCode == Event.VK_F && e.ctrlKey)) {
openSearch(false);
return true;
} else if (e.keyCode == Event.VK_S && e.ctrlKey && e.shiftKey) openStickers();
else if (e.keyCode == Event.VK_S && e.ctrlKey) openSmiles();
else if (e.keyCode == Event.VK_E && e.ctrlKey) openEmoji();
else if (e.keyCode == Event.VK_P && e.altKey) openPrefs("Chat", false);
else if (e.keyCode == Event.VK_I && e.altKey) openInfo();
else if (e.keyCode == Event.VK_Q && e.altKey) makeQuote(false, e.ctrlKey);
else if (e.keyCode == Event.VK_M && e.altKey) {
var page = getCurrentPage();
if (page && page.pagetype == "im") Native.ToggleSmiles();
} else if (e.keyCode == Event.VK_TAB && e.ctrlKey) {
setCurrentPage(e.shiftKey ? -1 : +1);
return true;
} else if (e.keyCode == 166 /* VK_BROWSER_BACK */ ||
(e.keyCode == Event.VK_LEFT && e.altKey) ||
(e.keyCode == Event.VK_F5 && e.ctrlKey)) {
setCurrentPage(-1);
return true;
} else if (e.keyCode == 167 /* VK_BROWSER_FORWARD */ ||
(e.keyCode == Event.VK_RIGHT && e.altKey) ||
(e.keyCode == Event.VK_F6 && e.ctrlKey)) {
setCurrentPage(+1);
return true;
}
}).on("keyup", () => { isModKey = false })
.on("appendevent prependevent updateevent updatesmiles showsearchhere showhistnotif updatemsgstatus switchtopage updatespell", function(e) {
if (e.type == "appendevent") e.target.addEvents(e.data, false);
else if (e.type == "prependevent") e.target.addEvents(e.data, true);
else if (e.type == "updateevent") e.target.updateEvents(e.data);
else if (e.type == "updatesmiles") { for (let page of pages.children) if (page.pagetype == "im") page.updateSmiles(); }
else if (e.type == "showhistnotif") e.target.showServerHistoryNotif();
else if (e.type == "showsearchhere") showSearchHere();
else if (e.type == "updatemsgstatus" && e.data !== null) e.target.updateMsgStatus(e.data);
else if (e.type == "switchtopage") e.target.setCurrent();
else if (e.type == "updatespell") e.target.updateSpelling(e.data);
})
.on("mouseenter", (e) => {
Native.OnChatMouseEnter();
}).on("mouseleave", (e) => {
Native.OnChatMouseLeave();
});
view.on("activate", (e) => {
isActive = e.reason !== 0;
Native.OnChatActivate(isActive);
}).on("size", () => {
if (videoview && videoview.init) videoview.alignPlayerControls();
Native.OnChatResize();
});
</script>
<popup id="videoinfo"></popup>
</head>
<body>
<div id="tabs"></div>
<div id="backTabs"></div>
<div id="pages" class="renderOption"></div>
<div id="buttonBar">
<div class="ddgroup first">
<button id="sendBtn" class="ddbutton"><div pic="status.unk"></div><span>Send</span></button>
<div id="sendVariant" class="ddpopup"></div>
</div>
<div class="ddgroup">
<button id="closeBtn" class="ddbutton"><div pic="close"></div><span>Close</span></button>
<div id="closeVariant" class="ddpopup"></div>
</div>
<div id="chatButtons">
<div hint="Search in history Ctrl+F"><div id="searchBtn" pic="search"></div></div>
<div hint="Shows smiles as pictures Alt+M, Smiles menu Ctrl+S"><div id="smilesBtn" pic="smiles"></div></div>
<div hint="Shows emoji as pictures Alt+M, Emoji menu Ctrl+E"><div id="emojiBtn" pic="emoji"></div></div>
<div hint="Shows stickers menu Ctrl+Shift+S, Stickers manager - RMB"><div id="stickersBtn" pic="stickers"></div></div>
<div hint="Preferences for the chat window Alt+P"><div id="prefBtn" pic="preferences"></div></div>
<div hint="Info about this user Alt+I"><div id="infoBtn" pic="info"></div></div>
<div hint="Quote previous received messages Alt+Q"><div id="quoteBtn" pic="quote"></div></div>
<div hint="Auto-close chat with this contact after sending a message. You can choose a default setting from the <Chat settings> - <Single messages>." class="checkable"><div id="singleBtn" pic="1.msg"></div></div>
<div hint="Send contacts"><div id="contactsBtn" pic="contacts"></div></div>
<div hint="Send pics"><div id="picsBtn" pic="pics"></div></div>
<div hint="Send file"><div id="fileBtn" pic="file"></div></div>
<div hint="Make screenshot"><div id="snapshotBtn" pic="snapshot"></div></div>
<div hint="Buzz contact"><div id="buzzBtn" pic="buzz"></div></div>
</div>
<div id="plugButtons"></div>
</div>
<status-bar>
<div id="sbChars"></div>
<div id="sbOutbox" hint="Outgoing messages queue"><div pic="outbox.empty"></div></div>
<div id="sbTrlt" hint="Transliteration (double click to toggle)"><div>TRLT</div></div>
<div id="sbCrypt" hint="Encryption status for current contact"><div id="pic"></div></div>
<div id="sbHint"></div>
</status-bar>
<menu id="histmenu" class="context custom">
<li id="hm_contactmenu"><div pic="ssi"></div><span>Contact menu...</span></li>
<li id="hm_copylink"><div pic="url"></div><span>Copy link</span></li>
<li id="hm_copy"><div pic="copy"></div><span>Copy</span></li>
<li id="hm_savepic"><div pic="pics"></div><span>Save pic</span></li>
<li id="hm_stickerpack"><div pic="stickers"></div><span>Find sticker pack</span></li>
<li id="hm_selectall"><div pic="select.all"></div><span>Select all</span></li>
<li id="hm_viewinwin"><div pic="msg"></div><span>View message in window</span></li>
<li id="hm_saveas"><div pic="save"></div><span>Save as</span><menu><li id="hm_saveastxt"><span>Text</span></li><li id="hm_saveashtml"><span>HTML</span></li></menu></li>
<li id="hm_addtofav"><div pic="theme"></div><span>Add link to favorites</span></li>
<li id="hm_edit"><div pic="typing"></div><span>Edit message</span></li>
<li id="hm_delete"><div pic="delete"></div><span>Delete selected</span></li>
<li id="hm_reactions"><div pic="reaction"></div><span>Refresh reactions</span></li>
<hr id="hm_hr_textsel">
<li id="hm_antispam"><div pic="msgbad"></div><span>Add to antispam</span></li>
<hr>
<li id="hm_imgsmiles"><div pic="smiles"></div><span>Show smiles and emoji as images</span></li>
<li id="hm_reltimes"><div pic="reltime"></div><span>Show relative timestamps</span></li>
<li id="hm_resethist"><div pic="reload.theme.list"></div><span>Reset loaded history</span></li>
<hr>
<li id="hm_openwith"><div pic="history"></div><span>Open chat with...</span></li>
<li id="hm_viewinfo"><div pic="info"></div><span>View info</span></li>
</menu>
<menu id="codemenu" class="context custom">
<li id="cm_bold"><span>Bold</span></li>
<li id="cm_underline"><span>Underlined</span></li>
<li id="cm_syntax"><span>Syntax highligh</span>
<menu>
<li id="cm_syntax_generic"><span>Generic</span></li>
<li id="cm_syntax_html"><span>HTML</span></li>
<li id="cm_syntax_js"><span>JavaScript</span></li>
<li id="cm_syntax_css"><span>CSS</span></li>
<li id="cm_syntax_json"><span>JSON</span></li>
<li id="cm_syntax_php"><span>PHP</span></li>
<li id="cm_syntax_delphi"><span>Delphi</span></li>
<li id="cm_syntax_c"><span>C</span></li>
</menu></li>
</menu>
<menu id="sendmenu" class="context custom">
<li value="1"><span>Multiple send</span></li>
<li value="2"><span>Send to all opened chats</span></li>
</menu>
<menu id="closemenu" class="context custom">
<li value="1"><span>Close all</span></li>
<li value="2"><span>Close all but this one</span></li>
<li value="3"><span>Close all offline</span></li>
<li value="4"><span>Close and add to ignore list</span></li>
<li value="5"><span>Close and add to ignore list those not in CL</span></li>
</menu>
<menu id="filemenu" class="context custom">
<li value="1"><span>Upload file</span></li>
<li value="2"><span>Archive and upload file(s)</span></li>
</menu>
<menu id="snapmenu" class="context custom">
<li value="10"><span>Entire screen</span>
<menu id="subscreens"></menu></li>
<li value="20"><span>Selected area</span></li>
</menu>
<menu id="inputmenu" class="context">
<div id="suggest"></div>
<li name="edit:undo" command="edit:undo"><label>Undo</label><span class="accesskey">&platform-cmd-mod;+Z</span></li>
<hr/>
<li name="edit:cut" command="edit:cut"><label>Cut</label><span class="accesskey">&platform-cmd-mod;+X</span></li>
<li name="edit:copy" command="edit:copy"><label>Copy</label><span class="accesskey">&platform-cmd-mod;+C</span></li>
<li name="edit:paste" command="edit:paste"><label>Paste</label><span class="accesskey">&platform-cmd-mod;+V</span></li>
<hr/>
<li name="edit:selectall" command="edit:selectall"><label>Select All</label><span class="accesskey">&platform-cmd-mod;+A</span></li>
</menu>
<include src="menus/contactmenu.htm" />
<popup class="imagegrid" id="smilesGrid">
<div class="imagelist" id="smilesList"><div></div></div>
</popup>
<popup class="imagegrid" id="emojiGrid">
<div class="imagecat" id="emojiCat"></div>
<div class="imagelist" id="emojiList"></div>
</popup>
<popup class="imagegrid" id="stickersGrid">
<div class="imagecat" id="stickersCat"></div>
<div class="imagelist" id="stickersList"></div>
</popup>
<div id="monIndicator"><div>2</div></div>
<div id="dragHtml" class="customHint dragHint"><span></span></div>
<div id="dragFile" class="customHint dragHint">
<div pic="file"></div>
<span></span>
</div>
<popup id="patches" class="customHint patchesHint renderOption"></popup>
<popup id="myreactions" class="customHint reactionsHint">
<div option="0"><div>👍</div><div class="delete" pic="deleted"></div></div>
<div option="1"><div></div><div class="delete" pic="deleted"></div></div>
<div option="2"><div>🤣</div><div class="delete" pic="deleted"></div></div>
<div option="3"><div>😳</div><div class="delete" pic="deleted"></div></div>
<div option="4"><div>😢</div><div class="delete" pic="deleted"></div></div>
<div option="5"><div>😡</div><div class="delete" pic="deleted"></div></div>
</popup>
<popup id="linkinfo" class="customHint infoHint">
<div class="table"></div>
<div class="miniavatar"></div>
</popup>
</body>
</html>