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/main.htm

1753 lines
52 KiB
HTML

<html>
<head>
<include src="common.htm" />
<style src="main.css" />
<style id="usercss" />
<style id="dyncss" />
<script src="hint.js" />
<script src="menus/contactmenu.js" />
<script>
const Native = View.share.MainNative;
view.uniqueid = "main";
view.minSize = [150, 75];
CommonNative.HideTaskbarButton(document);
setupWindow(true, false, false);
var body = document.body,
mainmenu = $("#mainmenu"),
statusmenu = $("#statusmenu"),
divmenu = $("#divmenu"),
groupmenu = $("#groupmenu"),
mstatus = $("#mstatus"),
mstatuses = $("#mstatuses"),
mthemes = $("#mthemes"),
mtoggle = $("#mtoggle"),
mshowemptygroups = $("#mshowemptygroups"),
cl = null,
bar = $("#bar"),
filter = $("#filter"),
clfilter = $("#clfilter"),
progress = $("#progress"),
windowMoving = false,
windowResizing = false,
keyword = "",
rebuilding = false,
dragging = false,
draggingOnline = false,
startX = 0, startY = 0,
startWinX = 0, startWinY = 0,
showIcons,
settings = {
onlineOnly: false,
combinedGroups: false,
showGroups: true,
showEmptyGroups: false,
showHints: true,
showStatusMsgMenu: true,
indentRoster: true,
autosize: false,
autosizeFull: false,
autosizeUp: false,
//animatedGroups: false,
collapseGroups: true,
blinkEnabled: true,
blinkWithStatus: true,
blinkTime: 600,
sortBy: 2,
barPos: 0,
filterPos: 2,
mainCSS: ""
},
hintFunc = function(e) {
if (settings.showHints && !dragging)
if (e.type == "mouseenter" || e.type == "mousedown") {
var opt = this.$p("option");
if (e.type == "mousedown") {
hideHint();
return false;
}
if (opt.isContact() || opt.isGroup()) {
showHint(this, opt.data);
return true;
}
return false;
} else if (e.type == "mouseleave") {
document.timer(200, hideHint);
}
}
class TreeNode extends Element {
ignoreBlur = false;
oldTitle;
this(props, kids) {
this.data = props.data;
}
// componentUpdate(newData = null) {
// if (newData) this.data = newData;
// this.patch(this.render());
// }
isDiv() {
return this.data.kind == NODE_DIV;
}
isGroup() {
return this.data.kind == NODE_GROUP;
}
isContact() {
return this.data.kind == NODE_CONTACT;
}
isLocal() {
return this.data.local;
}
needAuth() {
return this.data.needAuth;
}
getDiv() {
return this.data.divisor;
}
getGroup() {
return this.data.group;
}
getUID() {
return this.data.uid;
}
getOrder() {
return this.data.order;
}
getStatus() {
return this.data.status.code;
}
getLastAct() {
return this.data.lastActivity;
}
getCaption() {
if (this.isDiv())
return this.$("> caption .divtitle");
else if (this.isGroup())
return this.$("> caption .title");
else if (this.isContact())
return this.$("> caption .displayed");
}
startEditing(reason = GA_NONE) {
if (!this.isGroup() && !this.isContact()) return;
this.scrollIntoView({
block : "nearest",
behavior: "instant"
});
cl.refreshVisible();
this.post(() => {
this.classList.add("editing");
var title = this.getCaption();
var length = title.text.length;
this.oldTitle = title.text;
title.state.focus = true;
title.edit.selectRange(0, length);
title.on("blur", (e) => {
if (!this.ignoreBlur) this.stopEditing(true, reason);
}).on("keydown", (e) => {
if (e.keyCode == Event.VK_RETURN) {
this.stopEditing(true, reason);
return true;
} else if (e.keyCode == Event.VK_ESCAPE) {
this.stopEditing(false, reason);
return true;
}
});
});
}
cancelEditing(opt, title, restore) {
if (restore) title.text = opt.oldTitle;
opt.oldTitle = "";
opt.classList.remove("editing");
title.off("blur");
title.off("keydown");
cl.state.focus = true;
}
stopEditing(save, reason = GA_NONE) {
var title = this.getCaption();
var newTitle = title.text.trim();
if (save && newTitle != "") {
var res = true;
if (title.text != this.oldTitle)
if (this.isContact())
res = Native.RenameContact(this.getUID(), newTitle, reason);
else if (this.isGroup())
res = Native.RenameGroup(this.getGroup(), newTitle, reason);
if (res === false) {
this.cancelEditing(this, title, true);
} else if (res !== true) {
this.ignoreBlur = true;
createDialog(
"exclamation",
_("Warning"),
res
);
this.post(() => {
this.getCaption().state.focus = true;
this.ignoreBlur = false;
});
} else {
this.cancelEditing(this, title, false);
}
} else {
this.cancelEditing(this, title, true);
if (reason == GA_ADD) Native.RemoveGroup(this.getGroup());
}
}
componentDidMount() {
if (this.isGroup())
this.on("mousedown", "caption .icon", function() { cl.toggleGroup(this) });
this.first.on("mouseenter mouseleave ^mousedown", hintFunc);
}
componentWillUnmount() {
if (this.isGroup()) this.off("mousedown");
this.first.off(hintFunc);
}
}
class ThemedIcon extends Element {
img = null;
cls = null;
this(props) {
this.img = props.img;
this.cls = props.cls;
this.setupIcon();
}
setupIcon() {
if (this.img) {
this.applySprite(this.img);
this.requestPaint();
}
}
render() {
return <div class={this.cls}></div>;
}
}
class Status extends Element {
this(props) {
this.status = props.status;
this.setupIcon();
}
timerFunc() {
if (settings.blinkWithStatus) {
if (!this.status || !this.status.eventimg) return false;
//this.post(function() {
this.applySprite(this.state.expanded ? this.status.statusimg : this.status.eventimg);
this.state.expanded = !this.state.expanded;
// this.flushPaint();
//});
} else {
this.state.expanded = !this.state.expanded;
//this.flushPaint();
}
return true;
}
setupIcon() {
if (this.status.eventimg) {
this.applySprite(settings.blinkWithStatus ? this.status.statusimg : this.status.eventimg);
this.classList.add("blinking");
this.timer(settings.blinkTime, this.timerFunc);
} else {
this.classList.remove("blinking");
this.timer(0, this.timerFunc);
this.applySprite(this.status.statusimg);
}
this.requestPaint();
}
render() {
var mod = "";
if (this.status.deleted) mod = <div class="deleted"></div>
else if (this.status.official) mod = <div class="official"></div>
else if (this.status.bot) mod = <div class="bot"></div>
return <div key="1" class="status">{mod}</div>;
}
}
function isContactFiltered(data) {
if (keyword !== "")
if (data.displayed.toLowerCase().indexOf(keyword) === -1 &&
data.uid.toLowerCase().indexOf(keyword) === -1 &&
data.nick.toLowerCase().indexOf(keyword) === -1 &&
data.first.toLowerCase().indexOf(keyword) === -1 &&
data.last.toLowerCase().indexOf(keyword) === -1) return true;
return false;
}
class Contact extends TreeNode {
render() {
var cls = "contact";
if (isContactFiltered(this.data)) cls += " filtered";
var children = [];
for (let showIcon of showIcons)
if (showIcon[1])
switch (showIcon[0].idx) {
case 1:
children.push(<Status status={this.data.status} />);
break;
case 2:
if (this.data.xstatus.status != "") children.push(<div key="2" class="xstatus">{this.data.xstatus.status}</div>); break;
case 3:
if (this.data.needAuth) children.push(<div key="3" class="auth"></div>); break;
case 4:
if (this.data.local) children.push(<div key="4" class="local"></div>); break;
case 5: {
var dcls = "displayed";
if (this.data.noClient) dcls += " noclient";
children.push(<div key="5" class={dcls}>{this.data.displayed}</div>);
break;
}
case 6:
if (this.data.bdimg) children.push(<ThemedIcon key="6" img={this.data.bdimg} cls="birthday" />); break;
case 7:
if (this.data.hasAvatar) children.push(<img key="7" class="avatar" src={"miniavatar:" + this.data.uid} />); break;
case 8:
if (this.data.clientimg) children.push(<ThemedIcon key="8" img={this.data.clientimg} cls="client" />); break;
}
return <option key={this.data.hash} div={this.data.divisor} group={this.data.group} uid={this.data.uid} class={cls}><caption>{children}</caption></option>;
}
["on mousedragrequest"](e) {
if (this.getUID() == "0spamers") return;
hideHint();
dragging = true;
draggingOnline = CommonNative.IsOnline();
view.performDrag({ json: this.data }, "move", this);
dragging = false;
}
}
class Group extends TreeNode {
render() {
var contacts = [];
var empty = true;
var expanded = rebuilding ? this.data.expanded && (settings.combinedGroups || !settings.collapseGroups || (settings.collapseGroups && this.data.divisor != 1 && this.data.divisor != 2)) : this.data.expanded;
var contactsData = cldata.getContacts(this.data.divisor, this.data.group);
if (contactsData)
for (let contact in contactsData) {
if (expanded) contacts.push(<Contact data={contactsData[contact]} />);
if (empty) if (!isContactFiltered(contactsData[contact])) empty = false;
}
var count = cldata.getGroupCount(this.data.group, this.data.divisor).toString() + "/" + cldata.getTotalGroupCount(this.data.group);
var local = this.data.local ? <div class="local"></div> : "";
var cls = "group";
if (empty && keyword !== "") cls += " filtered";
return <option state-expanded={expanded} state-collapsed={!expanded} key={this.data.hash} div={this.data.divisor} group={this.data.group} class={cls}><caption><div class="icon"></div><div class="title">{this.data.displayed}</div>{local}<div class="count">{count}</div></caption>{contacts}</option>;
}
["on mousedragrequest"](e) {
hideHint();
dragging = true;
view.performDrag({ json: this.data }, "move", this.$("> caption"));
dragging = false;
}
["on dragaccept"](e) {
if (e.detail?.dataType !== "json") return false;
var data = e.detail.data;
if (data.group === this.getGroup()) return false;
if (data.kind == NODE_CONTACT)
return data.local || draggingOnline;
else if (data.kind == NODE_GROUP)
return true;
}
["on dragenter"](e) {
if (!dragging) return;
var data = e.detail.data;
if (data.kind == NODE_CONTACT)
this.classList.add("acceptcontact");
else if (data.kind == NODE_GROUP) {
var current = cl.getCurrent();
if (current)
this.classList.add(current.index > this.index ? "acceptgrouptop" : "acceptgroupbottom");
}
}
["on dragleave"](e) {
this.classList.remove("acceptcontact");
this.classList.remove("acceptgrouptop");
this.classList.remove("acceptgroupbottom");
}
["on drag"](e) {
dragScroll(e);
return true;
}
["on drop"](e) {
this.classList.remove("acceptcontact");
this.classList.remove("acceptgrouptop");
this.classList.remove("acceptgroupbottom");
var data = e.detail.data;
if (data.kind == NODE_CONTACT)
Native.MoveCurrentToGroup(this.getGroup());
else if (data.kind == NODE_GROUP) {
var current = cl.getCurrent();
if (!current) return;
var moveHigher = current.index > this.index;
this.post(() => {
Native.SetGroupOrder(data.group, this.getOrder() + (moveHigher ? -1 : +1))
});
}
}
}
class Divisor extends TreeNode {
render() {
var contacts = [];
var contactsData = cldata.getContacts(this.data.divisor);
if (contactsData)
for (let contact in contactsData)
contacts.push(<Contact data={contactsData[contact]} />);
var groups = [];
var groupsData = cldata.getGroups(this.data.divisor);
if (groupsData)
for (let group in groupsData) {
groups.push(<Group data={groupsData[group]} />);
// if (this.data.divisor == 0 || this.data.divisor == 4)
// console.log("div:", this.data.divisor, "group:", group, "hash:", groupsData[group].hash)
}
var count = 0;
var hasGroups = cldata.groups[this.data.divisor] ? cldata.groups[this.data.divisor].size > 0 : false;
for (let group in cldata.counts)
if (cldata.counts[group] && cldata.counts[group][this.data.divisor])
count += cldata.counts[group][this.data.divisor];
var cls = "divisor" + ((settings.showEmptyGroups ? (count > 0 || hasGroups) : count > 0) ? "": " filtered");
return <option state-expanded={true} state-collapsed={false} key={this.data.hash} div={this.data.divisor} class={cls}><caption><div class="fineline" /><div class="divtitle">{Native.DivisorToTitle(this.data.divisor)}</div><div class="divcount">{count}</div><div class="fineline" /></caption>{contacts}{groups}</option>;
}
}
class ArrayMap extends Map {
constructor(props) {
super(props);
return new Proxy(this, {
get(map, key) {
let value = map.has(key) ? map.get(key) : map[key];
if (typeof value === "function") value = value.bind(map);
return value;
},
set(map, key, value) {
map.set(key, value);
return true;
},
deleteProperty(map, key) {
if (map.has(key)) {
map.delete(key);
return true;
} else return false;
},
has(map, key) {
return map.has(key);
},
ownKeys(map, key) {
return Array.from(map.keys());
},
enumerate(map) {
return map[Symbol.iterator || '@@iterator'];
},
getOwnPropertyDescriptor(map, key) {
return { enumerable: true, configurable: true, value: map[key] };
}
});
}
}
class VirtualTreeData {
divisors = new ArrayMap();
groups = new ArrayMap();
contacts = new ArrayMap();
counts = new ArrayMap();
putData(data, rebuild = false) {
if (data.kind == NODE_DIV) {
this.divisors[data.divisor] = data;
if (!rebuild) this.sortDivisors();
} else if (data.kind == NODE_GROUP) {
if (!this.groups[data.divisor]) this.groups[data.divisor] = new ArrayMap();
this.groups[data.divisor][data.group] = data;
if (!rebuild && settings.showGroups) this.sortGroups(data.divisor);
} else if (data.kind == NODE_CONTACT) {
if (!this.contacts[data.divisor]) this.contacts[data.divisor] = new ArrayMap();
if (!this.contacts[data.divisor][data.group]) this.contacts[data.divisor][data.group] = new ArrayMap();
if (!this.contacts[data.divisor][data.group][data.uid]) this.changeCount(data, "inc");
this.contacts[data.divisor][data.group][data.uid] = data;
if (!rebuild) this.sortContacts(data.divisor, data.group);
}
}
getData(kind, div, group = 0, uid = "") {
if (kind == NODE_DIV)
return this.divisors[div];
else if (kind == NODE_GROUP)
return this.groups[div] ? this.groups[div][group] : null;
else if (kind == NODE_CONTACT)
return this.contacts[div] && this.contacts[div][group] ? this.contacts[div][group][uid] : null;
}
getContacts(div, group = 0) {
return this.contacts[div] ? this.contacts[div][group] : null;
}
getGroups(div) {
return this.groups[div];
}
removeData(data, dec = true) {
try {
if (data.kind == NODE_DIV) {
delete this.divisors[data.divisor];
delete this.groups[data.divisor];
delete this.contacts[data.divisor];
} else if (data.kind == NODE_GROUP) {
delete this.groups[data.divisor][data.group];
delete this.contacts[data.divisor][data.group];
} else if (data.kind == NODE_CONTACT) {
delete this.contacts[data.divisor][data.group][data.uid];
if (dec) this.changeCount(data, "dec");
}
} catch(e) {
console.error(e, e.stack);
}
}
sortDivisors() {
if (this.divisors)
this.divisors = new ArrayMap([...this.divisors.entries()].sort((pair1, pair2) => {
var data1 = pair1[1];
var data2 = pair2[1];
return data1.order < data2.order ? -1 : (data1.order > data2.order ? +1 : 0);
}));
}
sortGroups(divisor) {
if (this.groups[divisor])
this.groups[divisor] = new ArrayMap([...this.groups[divisor].entries()].sort((pair1, pair2) => {
var data1 = pair1[1];
var data2 = pair2[1];
if (data1.order == data2.order)
return data1.displayed.toLowerCase().localeCompare(data2.displayed.toLowerCase());
else
return data1.order < data2.order ? -1 : +1;
}));
}
sortContacts(divisor, group) {
var contactlist = this.contacts[divisor] ? this.contacts[divisor][group] : null;
if (contactlist)
this.contacts[divisor][group] = new ArrayMap([...contactlist.entries()].sort((pair1, pair2) => {
var weight = 0;
var data1 = pair1[1];
var data2 = pair2[1];
if (settings.combinedGroups) {
if (data1.status.code == 0) weight += -1;
if (data2.status.code == 0) weight += +1;
if ((data1.status.code == 0 || data2.status.code == 0) && data1.status.code != data2.status.code) return weight;
}
if (data1.order != data2.order) return data1.order < data2.order ? -1 : +1;
if (settings.sortBy == 0) return weight;
if (settings.sortBy == 1 || (settings.sortBy == 2 && data1.lastActivity == 0 && data2.lastActivity == 0))
weight += data1.displayed.toLowerCase().localeCompare(data2.displayed.toLowerCase());
else if (settings.sortBy == 2)
weight += data1.lastActivity < data2.lastActivity ? +1 : -1;
return weight;
}));
}
sort() {
//var start = new Date().valueOf();
this.sortDivisors();
if (settings.showGroups) {
for (let divisor in this.groups) {
this.sortGroups(divisor);
for (let group in this.groups[divisor]) this.sortContacts(divisor, group);
}
} else {
for (let divisor in this.divisors) this.sortContacts(divisor, 0);
}
// var end = new Date().valueOf();
// console.log("cldata sort", end - start);
}
changeCount(data, mod) {
if (mod == "inc") {
if (!this.counts[data.group]) this.counts[data.group] = new ArrayMap();
if (!this.counts[data.group][data.divisor]) this.counts[data.group][data.divisor] = 0;
if (!this.counts[data.group]["total"]) this.counts[data.group]["total"] = 0;
this.counts[data.group][data.divisor]++;
this.counts[data.group]["total"]++;
} else if (mod == "dec" && this.counts[data.group]) {
this.counts[data.group][data.divisor]--;
this.counts[data.group]["total"]--;
}
}
getGroupCount(group, divisor) {
return this.counts[group] && cldata.counts[group][divisor] ? cldata.counts[group][divisor] : 0;
}
getTotalGroupCount(group) {
return this.counts[group] && cldata.counts[group]["total"] ? cldata.counts[group]["total"] : 0;
}
clear() {
this.divisors = new ArrayMap();
this.groups = new ArrayMap();
this.contacts = new ArrayMap();
this.counts = new ArrayMap();
}
}
var cldata = new VirtualTreeData();
class VirtualTree extends Element {
allContacts = [];
editNode(data, reason) {
var opt = this.getOption(data);
if (opt) {
opt.execCommand("set-current");
opt.startEditing(reason);
}
}
toggleGroup(opt, force = "auto") {
if (opt.state.expanded && (force == "auto" || force == "collapse")) {
opt.state.collapsed = true;
Native.GroupStateChanged(false, opt.getGroup(), opt.getDiv());
} else if (opt.state.collapsed && (force == "auto" || force == "expand")) {
//var start = new Date().valueOf();
opt.state.expanded = true;
Native.GroupStateChanged(true, opt.getGroup(), opt.getDiv());
//var end = new Date().valueOf();
//debug expand: opt.@#group, end - start;
}
}
toggleAll(action) {
var selected = this.getCurrent();
for (let opt of this.$$("option.group"))
if (!selected.isDiv() || (selected.isDiv() && opt.getDiv() == selected.getDiv()))
this.toggleGroup(opt, action);
}
getDivisor(data) {
return this.$(`> option.divisor[div="${data.divisor}"]`);
}
getGroup(data) {
if (data.group == 0) return null;
var div = this.getDivisor(data);
return div ? div.$(`> option.group[group="${data.group}"]`) : null;
}
getContact(data) {
var contact;
var div = this.getDivisor(data);
if (div) {
contact = div.$(`> option.contact[group="${data.group}"][uid="${data.uid}"]`);
if (contact) return contact;
}
var group = this.getGroup(data);
return group ? group.$(`> option.contact[group="${data.group}"][uid="${data.uid}"]`) : contact;
}
getContactByUIN(uin) {
return this.$(`option.contact[uid="${uin}"]`);
}
getOption(data) {
var opt = null;
if (data.kind == NODE_DIV)
opt = this.getDivisor(data);
else if (data.kind == NODE_GROUP)
opt = this.getGroup(data);
else if (data.kind == NODE_CONTACT)
opt = this.getContact(data);
return opt;
}
getCurrent() {
return this.$("option:current");
}
render() {
var divs = [];
for (let divisor in cldata.divisors)
divs.push(<Divisor data={cldata.divisors[divisor]} />);
var cls = settings.blinkWithStatus ? "statusblink" : "eventblink";
if (settings.onlineOnly) cls += " onlineOnly";
if (settings.indentRoster) cls += " groupPadding";
// if (settings.animatedGroups) cls += " animated";
return <widget id="cl" class={cls} type="tree">{divs}</widget>;
}
refreshAllContacts() {
this.allContacts = this.$$("option.contact");
}
queueUpdate() {
this.timer(250ms, this.componentUpdate);
}
refreshVisible() {
var overshoot = 180;
var treeHeight = cl.state.box("height", "client");
for (let contact of this.allContacts) {
var [cntLeft, cntTop, cntRight, cntBottom] = contact.state.box("rect", "border", cl);
if (cntBottom < -overshoot || cntTop > treeHeight + overshoot)
contact.classList.add("hidden");
else
contact.classList.remove("hidden");
}
}
executeAction(opt) {
if (opt.isDiv()) {
Native.ToggleOnlineOnly();
return true;
} else if (opt.isGroup()) {
cl.toggleGroup(opt);
return true;
} else if (opt.isContact()) {
Native.OpenContact(opt.getUID());
return true;
}
return false;
}
setClicked(opt) {
if (opt)
Native.SetClicked(
opt.isContact() ? NODE_CONTACT : opt.isGroup() ? NODE_GROUP : opt.isDiv() ? NODE_DIV : 0,
opt.getDiv(),
opt.isGroup() ? opt.getGroup() : 0,
opt.isContact() ? opt.getUID() : ""
); else Native.SetClicked(NODE_DIV, 0, 0, "");
}
componentDidMount() {
cl = this;
}
componentDidUpdate() {
this.refreshAllContacts();
if (keyword !== "")
for (let div of this.children) if (div) {
var empty = true;
for (let opt of div.children)
if (opt && opt.tag === "option" && !opt.classList.contains("filtered")) {
empty = false;
break;
}
if (empty) div.classList.add("filtered");
}
this.onsizechange();
}
["on contextmenu"](e) {
openContextMenu(e.reason == Event.BY_MOUSE_CLICK);
return true;
}
["on ^dblclick at option > caption"](e, el) {
return e.mainButton ? this.executeAction(el.parent) : false;
}
["on ^mousedown"](e) {
if (!e.propButton) return;
var opt = e.target.$p("option");
if (opt) {
opt.execCommand("set-current");
this.setClicked(opt);
} else {
cl.value = null;
this.setClicked(null);
}
}
onmouseleave(e) {
if (settings.showHints) hideHint();
}
onwheel(e) {
if (settings.showHints) hideHint();
}
onscroll(e) {
this.post(this.refreshVisible);
}
onsizechange(e) {
if (settings.autosize && !windowResizing)
this.timer(32ms, doAutosize);
else
this.post(this.refreshVisible);
}
onkeydown(e) {
var selected = this.getCurrent();
if (!selected) return false;
if (selected.classList.contains("editing")) return true;
if (e.keyCode == Event.VK_RETURN) {
return this.executeAction(selected);
} else if (e.keyCode == Event.VK_LEFT || e.keyCode == Event.VK_RIGHT) {
if ((e.keyCode == Event.VK_LEFT && selected.state.expanded) ||
(e.keyCode == Event.VK_RIGHT && selected.state.collapsed))
if (selected.isGroup()) this.toggleGroup(selected);
return true;
} else if (e.keyCode == Event.VK_DELETE) {
if (selected.isGroup()) Native.DeleteCurrentGroup();
else if (selected.isContact()) Native.DeleteCurrentContact();
} else if (e.keyCode == Event.VK_F2) {
var opt = this.getCurrent();
if (!opt) return false;
if (opt.isGroup()) {
if (!opt.isLocal() && !CommonNative.IsOnline())
createDialog(
"asterisk",
_("Information"),
_("Group is not local and must be modified while online"),
true
);
else
opt.startEditing(GA_RENAME);
} else opt.startEditing();
return true;
}
return false;
}
oncontentchange() {
this.timer(32ms, doAutosize);
}
onchange(e) {
var selected = this.getCurrent();
this.setClicked(selected);
this.post(this.refreshVisible);
}
["on dragaccept"](e) {
return e.detail?.dataType === "json";
}
["on drag"](e) {
dragScroll(e);
return false;
}
["on drop"](e) {
return false;
}
}
$("#cl").patch(<VirtualTree />);
//$("#cl").replaceWith(Element.create());
cl = $("#cl");
function dragScroll(e) {
var onedip = 1dip.valueOf();
var [x, y, w, h] = cl.state.box("rectw", "border", "window");
var inc = e.yView < y + 20 * onedip ? -15 : (e.yView > y + h - 20 * onedip ? +15 : 0);
if ((inc < 0 && cl.scroll("top") == 0) || (inc > 0 && cl.scroll("bottom") == 0)) return;
if (inc != 0) cl.scrollTo(0, cl.scroll("top") + inc * onedip, true);
}
function clear() {
cldata.clear();
if (cl) cl.componentUpdate();
}
function insertNode(data, rebuild) {
cldata.putData(data, rebuild);
if (rebuild) return;
//console.log("insertNode:", data.hash, data.kind, data.divisor, data.group, data.uid);
cl.componentUpdate();
}
function removeNode(data) {
//console.log("removeNode:", data.hash, data.kind, data.divisor, data.group, data.uid);
cldata.removeData(data);
cl.componentUpdate();
}
function editNode(data, reason = GA_NONE) {
cl.editNode(data, reason);
}
function finishBuild() {
rebuilding = true;
cldata.sort();
cl.componentUpdate();
rebuilding = false;
}
function focusFilter() {
if (clfilter.state.visible)
clfilter.state.focus = true;
else
cl.state.focus = true;
}
function focusNode(data) {
var opt = cl.getOption(data);
if (opt) opt.post(function() { this.execCommand("set-current") });
}
function setGroupState(data, expand) {
var opt = cl.getGroup(data);
if (opt) cl.toggleGroup(opt, expand ? "expand" : "collapse");
}
function getCurrentContact() {
var selected = cl.getCurrent();
return selected && selected.isContact() ? selected.getUID() : "";
}
function getContacts(div) {
var uids = [];
try {
if (cldata.contacts[div])
for (let group in cldata.contacts[div])
for (let uid in cldata.contacts[div][group])
uids.push(uid);
} catch(e) {
console.error(e, e.stack);
}
return uids;
}
function initSettings(icons, sets) {
showIcons = icons;
settings = sets;
var newFrame = settings.noBorderShadow ? "solid-with-shadow" : "solid";
if (view.frameType != "standard" && view.frameType != newFrame)
view.frameType = newFrame;
view.isTopmost = settings.alwaysOnTop;
$("#mlocalhelp").setVisible(settings.helpExists);
$("#mnewgroup").state.disabled = !settings.showGroups;
var pic1 = $("#mshowgroups").$("div[pic]");
var pic2 = $("#mshowgroups2").$("div[pic]");
if (settings.showGroups) {
pic1.setIcon("right");
pic2.setIcon("right");
mshowemptygroups.state.disabled = false;
} else {
pic1.clearSprite();
pic2.clearSprite();
mshowemptygroups.state.disabled = true;
}
var pic = mshowemptygroups.$("div[pic]");
if (settings.showEmptyGroups) pic.setIcon("right"); else pic.clearSprite()
pic = $("#monlineonly").$("div[pic]");
if (settings.onlineOnly) pic.setIcon("right"); else pic.clearSprite()
pic = $("#mseparation").$("div[pic]");
if (settings.combinedGroups) pic.setIcon("right"); else pic.clearSprite()
bar.setVisible(settings.barPos != 2);
if (settings.barPos < 2) {
bar.detach();
if (settings.barPos == 0)
body.prepend(bar);
else
body.insert(bar, body.$("> menu").index);
}
filter.setVisible(settings.filterPos != 2);
if (settings.filterPos < 2) {
filter.detach();
if (settings.filterPos == 0) {
if (settings.barPos == 0)
body.insert(filter, 1);
else
body.prepend(filter);
} else {
if (settings.barPos == 1)
body.insert(filter, 1);
else
body.insert(filter, body.$("> menu").index);
}
}
if (settings.filterPos == 2 && keyword !== "") {
keyword = "";
clfilter.value = "";
}
var usercss = $("style#usercss");
usercss.text = settings.mainCSS;
if (cl) cl.componentUpdate();
}
function initMenus(statuses) {
var options = [];
for (let status of statuses)
options.push(<li code={status.code}><div auto pic={status.img}></div><span>{status.caption}</span></li>);
options.push(<hr/>);
options.push(<li class="visibility"><div pic></div><span></span></li>);
if (settings.showStatusMsgMenu) {
options.push(<hr/>);
options.push(<li><div class="xstatus"></div><span>{_("Message")}</span></li>);
}
statusmenu.patch(<menu>{options}</menu>, true);
mstatuses.patch(<menu>{options}</menu>, true);
}
function updateMenus(visible) {
mtoggle.$("> span").text = _(visible ? "Hide" : "Show");
mtoggle.$("> div").setIcon(visible ? "minimize" : "restore");
}
function initThemeList(themes, smiles, emojis, sounds) {
var oldthemes = mthemes.$$(".theme");
for (let oldtheme of oldthemes) oldtheme.remove();
if (themes)
for (let theme of themes) {
var check = theme.current ? "checked" : "unchecked";
mthemes.append(<li class="theme" index={theme.index}><div pic={check}></div><span>{theme.title}</span></li>);
}
var msmileslist = $("#msmileslist");
msmileslist.clear();
if (smiles)
for (let smile of smiles) {
var check = smile.current ? "checked" : "unchecked";
msmileslist.append(<li index={smile.index}><div pic={check}></div><span>{smile.title}</span></li>);
}
var memojislist = $("#memojislist");
memojislist.clear();
if (emojis)
for (let emoji of emojis) {
var check = emoji.current ? "checked" : "unchecked";
memojislist.append(<li index={emoji.index}><div pic={check}></div><span>{emoji.title}</span></li>);
}
var msoundslist = $("#msoundslist");
msoundslist.clear();
if (sounds)
for (let sound of sounds) {
var check = sound.current ? "checked" : "unchecked";
msoundslist.append(<li index={sound.index}><div pic={check}></div><span>{sound.title}</span></li>);
}
var pics = mthemes.$$("div[pic]");
for (let pic of pics) pic.setupIcon();
}
function applyTheme(offPic, botPic, delPic, authPic, localPic) {
// for (let icon of $$("menu.custom li")) {
// if (icon) icon.applySprite(getSpriteData(icon.attr["pic"]));
// }
var dyncss = $("style#dyncss");
dyncss.text = makeSpriteStyle("#cl option.contact > caption div.official", offPic);
dyncss.text += makeSpriteStyle("#cl option.contact > caption div.bot", botPic);
dyncss.text += makeSpriteStyle("#cl option.contact > caption div.deleted", delPic);
dyncss.text += makeSpriteStyle("#cl option.contact > caption div.auth", authPic);
dyncss.text += makeSpriteStyle("#cl option > caption div.local", localPic);
var groupClosed = getSpriteData("close.group");
var groupOpened = getSpriteData("open.group");
dyncss.text += makeSpriteStyle("#cl option.group:collapsed > caption .icon", groupClosed);
dyncss.text += makeSpriteStyle("#cl option.group:expanded > caption .icon", groupOpened);
var pics = $$("div[pic]");
for (let pic of pics) pic.setupIcon();
var bimages = $$("body, [style], div[pic]");
var themepics = [];
for (let bimage of bimages)
if (bimage) {
var link = bimage.style.backgroundImage;
if (!link) continue;
if (link.indexOf("url(") === 0)
link = link.match(/^url\((.*)\)$/)[1];
if (link.indexOf("themepic:") === -1 || themepics.indexOf(link) !== -1) continue;
themepics.push(link);
}
for (let themepic of themepics) reloadImage(themepic);
themepics = [];
}
function setProgress(progress) {
$("#pvalue").text = progress == 0 ? "" : Math.round(progress * 100) + "%";
$("#pslider").value = progress;
$("#pslider").flushPaint();
}
function updateContactCount(str) {
$("#ccount").text = str;
}
function updateStatusImage(pic) {
$("#statusBtn").$("> div[pic]").setIcon(pic);
mstatus.setIcon(pic);
}
function updateVisibilityImage(isVis) {
var visibilities = $$(".visibility");
for (let visibility of visibilities) {
visibility.$("> div[pic]").setIcon(isVis ? "visibility.all" : "visibility.none");
visibility.$("> span").text = _(isVis ? "Visible" : "Invisible");
}
}
function updateAdditionalImage(xst, pic) {
var xstatuses = $$("menu .xstatus");
for (let xstatus of xstatuses)
if (xstatus.$p("#statusmenu") || xstatus.$p("#mstatuses"))
xstatus.text = xst;
var addBtn = $("#addBtn");
if (pic === "") {
addBtn.classList.add("collapsed");
} else {
addBtn.classList.remove("collapsed");
var sprite = addBtn.$("> div[pic]");
var emoji = addBtn.$("> div.emoji");
if (pic === "outbox") {
emoji.setVisible(false);
sprite.setVisible(true);
sprite.setIcon(pic);
} else {
sprite.setVisible(false);
emoji.setVisible(true);
emoji.text = pic;
}
addBtn.requestPaint();
}
}
function openMainMenu(el = null) {
var pos, x, y;
if (el) {
var onedip = 1dip.valueOf();
[x, y] = el.state.box("position", "padding", "window");
var [w, h] = el.state.box("dimension", "padding");
var [ex, ey] = el.state.box("position", "padding", "screen");
var [sx, sy, sw, sh] = view.screenBox("frame", "rectw");
if (ey > sy + sh / 2) {
y += h;
if (ex < sx + sw / 2) {
y += onedip;
x += w - onedip;
pos = 1;
} else {
y += onedip;
x += onedip;
pos = 3;
}
} else {
y -= onedip;
if (ex < sx + sw / 2) {
x += w - onedip;
pos = 7;
} else {
x += onedip;
pos = 9;
}
}
} else {
[x, y] = view.box("position", "cursor", "desktop");
var scr = getScreenFromCoord(x, y);
var [sx, sy, sw, sh] = View.screenBox(scr, "frame", "rectw");
pos = x > sx + sw / 2 ? (y > sy + sh / 2 ? 3 : 9) : (y > sy + sh / 2 ? 1 : 7);
var [x, y] = coordScreenToView(x, y);
}
document.popup(mainmenu, {
anchorAt: 7,
popupAt: pos,
x: x,
y: y
});
}
function openStatusMenu(el = null) {
var pos, x, y, h;
var onedip = 1dip.valueOf();
if (el) {
[x, y] = el.state.box("position", "border", "window");
h = el.state.box("height", "border");
pos = 7;
x -= onedip;
y += h - onedip;
} else {
[x, y] = view.box("position", "cursor");
pos = 7;
}
statusmenu.flushPaint();
var oldpos = pos;
[pos, x, y] = getPopupPosition(x, y, statusmenu);
if (el && oldpos == 7 && pos == 1) y -= h - onedip * 2;
document.popup(statusmenu, {
anchorAt: 7,
popupAt: pos,
x: x,
y: y
});
}
function openContextMenu(followMouse = false) {
var selected = cl ? cl.getCurrent() : null, x, y;
if (followMouse || !selected) {
[x, y] = view.box("position", "cursor");
} else {
[x, y] = selected.state.box("position", "border", "window");
x -= 1;
y += selected.$("> caption").state.box("height", "border");
}
if (!selected || selected.isDiv()) {
var pos = 7;
[pos, x, y] = getPopupPosition(x, y, divmenu);
document.popup(divmenu, {
anchorAt: 7,
popupAt: pos,
x: x,
y: y
});
} else if (selected.isGroup()) {
var group = selected.getGroup();
if (group == 0) return;
var addToServer = $("#maddgrouptoserver");
var removeFromServer = $("#mremovegroupfromserver");
var renameGroup = $("#mrenamegroup");
var deleteGroup = $("#mdeletegroup");
var isOnline = CommonNative.IsOnline();
$("#mmoveallto").state.disabled = !isOnline;
renderGroupLists($("#mgrouplist"), false);
if (selected.isLocal()) {
addToServer.setVisible(true);
addToServer.state.disabled = !isOnline;
removeFromServer.setVisible(false);
renameGroup.state.disabled = false;
deleteGroup.state.disabled = false;
} else {
addToServer.setVisible(false);
removeFromServer.setVisible(true);
removeFromServer.state.disabled = !isOnline;
renameGroup.state.disabled = !isOnline;
deleteGroup.state.disabled = !isOnline;
}
var pos = 7;
[pos, x, y] = getPopupPosition(x, y, groupmenu);
document.popup(groupmenu, {
anchorAt: 7,
popupAt: pos,
x: x,
y: y
});
} else if (selected.isContact()) {
var uid = selected.getUID();
if (uid) openContactMenu(uid, x, y);
}
}
function openStatusDialog() {
openSingleWindow({
url: "status.htm",
caption: "XStatus and status message",
uniqueid: "statusmessage",
dialog: true,
alignment: 5,
x: view.box("left", "border", "desktop"),
y: view.box("top", "border", "desktop"),
width: 270,
height: 350,
params: {
mainThis: this,
parentWidth: view.box("width", "border", "desktop"),
parentHeight: view.box("height", "border", "desktop")
}
});
}
function openAddContactDialog(nick = null) {
openSingleWindow({
url: "addcontact.htm",
caption: "Add contact",
uniqueid: "addcontact",
alignment: 5,
params: {
nick: nick
}
});
}
function btnClickedEvent(e) {
btnClicked(this, e.propButton);
}
function btnClicked(el, prop) {
if (el.id == "menuBtn") {
openMainMenu(el);
return;
} else if (el.id == "statusBtn") {
if (prop)
openStatusDialog();
else
openStatusMenu(el);
return;
} else if (el.id == "addBtn" && prop) {
openStatusDialog();
return;
}
var [x, y] = el.state.box("position", "border", "screen");
Native.BarButtonClick(el.id, prop, x, y);
}
// function startCoverage() {
// VM.startStats("coverage");
// }
// function getCoverage() {
// var coverage = VM.getStats("coverage");
// debug coverage: coverage;
// }
function getWindowHeight() {
var h = 0;
if (!settings.autosizeFull) {
var d_online = cl.$("> option.divisor[div=\"0\"]");
if (!d_online) return -1;
if (d_online.state.visible) h += d_online.state.box("height", "margin");
var d_nil = cl.$("> option.divisor[div=\"3\"]");
if (d_nil && d_nil.state.visible) h += d_nil.state.box("height", "margin");
var d_recents = cl.$("> option.divisor[div=\"4\"]");
if (d_recents && d_recents.state.visible) h += d_recents.state.box("height", "margin");
} else {
h = cl.state.box("height", "content");
}
var p1 = body.style["padding-top"].valueOf();
var p2 = body.style["padding-bottom"].valueOf();
var b = bar.state.visible ? bar.state.box("height", "margin") : 0;
var f = filter.state.visible ? filter.state.box("height", "margin") : 0;
return h == 0 ? -1 : b + h + f + p1 + p2;
}
function doAutosize() {
if (!settings.autosize) return;
var y = getWindowHeight();
if (y <= 20) return;
y += 3;
var [left, top, width, height] = view.box("rectw", "client", "desktop");
if (y == height) return;
var [wx1, wy1, wx2, wy2] = view.screenBox("workarea", "rect");
var gap = 20dip.valueOf();
if (settings.autosizeUp) {
var limit = top + height - wy1 - gap;
if (y > limit) y = limit;
var delta = y - height;
view.move(left, top - delta, width, y, true);
} else {
var limit = wy2 - top - gap;
if (y > limit) y = limit;
view.move(left, top, width, y, true);
}
cl.post(cl.refreshVisible);
}
function updateCaption(caption) {
view.caption = caption;
}
function showHintForCurrent() {
var current = cl.getCurrent();
if (current) showHint(cl, current.data);
}
function show() {
view.state = View.WINDOW_SHOWN;
updateMenus(true);
Native.OnCLShow();
}
function hide() {
view.state = View.WINDOW_HIDDEN;
updateMenus(false);
Native.OnCLHide();
}
function setBorder(hasBorder) {
// document.post(() => {
// view.frameType = "solid-with-shadow";
// CommonNative.SetLargeWindowIcon(document, ""); // SendMessage(Handle, WM_SETICON, ICON_BIG, IconHandle)
// view.frameType = "standard";
// });
//return;
if (hasBorder) {
CommonNative.SetLargeWindowIcon(document, "-");
view.frameType = "standard";
} else
document.post(() => {
CommonNative.SetLargeWindowIcon(document, "-");
var newFrame = settings.noBorderShadow ? "solid-with-shadow" : "solid";
if (view.frameType != newFrame) view.frameType = newFrame;
}); // Sciter's bug
}
function tryToQuit() {
if (!View.share.commonSettings.quitConfirmation || createDialog(
"question",
_("Confirm"),
_("Really quit?"),
false, ["mbYes", "mbNo"], "mbYes", "mbNo"
) === "mbYes") Native.Quit();
}
var isModKey = false;
document.on("keydown", (e) => {
isModKey = e.ctrlKey || e.shiftKey;
if (e.keyCode == Event.VK_ESCAPE) {
if (windowMoving) {
progress.classList.remove("moving");
progress.state.capture(false);
windowMoving = false;
return true;
}
if (clfilter.state.focus) return false;
if (view.state == View.WINDOW_HIDDEN)
show();
else
hide();
return true;
} else if (e.ctrlKey && e.keyCode == Event.VK_F)
Native.ToggleFilter();
else if (e.altKey && e.keyCode == Event.VK_F4)
tryToQuit();
else
CommonNative.ProcessMacro(e.platformKeyCode, e.shiftKey, e.ctrlKey, e.altKey, e.metaKey);
})
.on("keyup", (e) => { if (e.keyCode != Event.VK_CONTEXT_MENU) isModKey = false })
.on("mouseenter", (e) => { Native.OnCLMouseEnter(); })
.on("mouseleave", (e) => { Native.OnCLMouseLeave(); })
.on("closerequest", function(e){
e.preventDefault();
if (e.reason == Event.REASON_BY_CHROME)
hide();
else
tryToQuit();
});
function updateTranslation() {
translateWindow("menu.custom li > span, menu.context li > label, menu.custom hr > span, menu.custom caption");
}
updateTranslation();
$("#addBtn").classList.add("collapsed");
$("#addBtn").on("mouseup", btnClickedEvent);
$("#menuBtn").on("mouseup", btnClickedEvent);
$("#statusBtn").on("mouseup", btnClickedEvent);
$("#clearBtn").on("click", (e) => {
keyword = "";
clfilter.value = "";
clfilter.state.focus = true;
cl.componentUpdate();
});
bar.on("mouseup", (e) => {
var isBtn = e.target.classList.contains("imgbtn") || (e.target.parent && e.target.parent.classList.contains("imgbtn"));
if (!isBtn && e.propButton) btnClicked($("#menuBtn"), false);
});
clfilter.on("change", function(e) {
keyword = this.value.toLowerCase();
cl.queueUpdate();
}).on("keydown", function(e) {
if (e.keyCode == Event.VK_ESCAPE) {
this.post(() => { Native.ToggleFilter() });
return true;
}
});
progress.on("^mousedown", function(e) {
if (!e.mainButton) return;
windowMoving = true;
this.classList.add("moving");
[startX, startY] = view.box("position", "cursor");
[startWinX, startWinY] = view.box("position", "client", "desktop");
startX += startWinX;
startY += startWinY;
this.state.capture("strict");
}).on("^mouseup", function(e) {
if (!windowMoving) return;
this.classList.remove("moving");
this.state.capture(false);
windowMoving = false;
}).on("mousemove", function(e) {
if (!windowMoving) return;
var [x, y] = view.box("position", "cursor");
var [winX, winY] = view.box("position", "client", "desktop");
view.move(startWinX + winX + x - startX, startWinY + winY + y - startY, true);
return true;
}).on("dblclick", function(e) {
if (e.mainButton) Native.GoOnline();
});
setMenuListener(mainmenu, function() {
var index = this.attr["index"];
if (this.parent)
switch (this.parent.id) {
case "mstatuses":
if (this.$(".xstatus"))
openStatusDialog();
else
Native.SetStatus(parseInt(this.attr["code"]));
return;
case "mthemes":
if (index) { Native.SelectTheme(parseInt(index)); return; }
break;
case "msmileslist":
if (index) { Native.SelectSmiles(parseInt(index)); return; }
break;
case "memojislist":
if (index) { Native.SelectEmojis(parseInt(index)); return; }
break;
case "msoundslist":
if (index) { Native.SelectSounds(parseInt(index)); return; }
break;
}
switch (this.id) {
case "madduin": openAddContactDialog(); break;
case "mprotopassword": openLoginDialog(); break;
case "maccpassword": openAccDialog(); break;
case "mlock": Native.Lock(); break;
case "mviewinfoabout": viewInfoAbout(); break;
case "mopenchat": openChatWith(); break;
case "msearch": openHistorySearch(); break;
case "moutbox": openOutbox(); break;
case "mdb": openContactsDB(); break;
case "mlog": openLog(); break;
case "msessionsmgr": openSessionsManager(); break;
case "mreloadlang": Native.ReloadLang(); break;
case "mgetcl": Native.GetCL(); break;
case "mreloadtheme": Native.ReloadTheme(); break;
case "mthemelist": Native.ReloadThemeList(); break;
case "mcontactthemes": Native.OpenContactThemes(); break;
case "musers": CommonNative.ChangeOrAddUser(); break;
case "mprefs": openPrefs(); break;
case "mmyinfo": Native.OpenMyInfo(); break;
case "mmyprofile": Native.OpenMyWebProfile(); break;
case "mupdate": openUpdater(); break;
case "mportal": CommonNative.OpenPortal(); break;
case "mhelp": Native.OpenHelpSite(); break;
case "mlocalhelp": Native.OpenHelpLocal(); break;
case "mabout": openAbout(); break;
case "mtoggle": Native.Toggle(); break;
case "mexit": tryToQuit(); break;
}
});
setMenuListener(statusmenu, function() {
if (this.$(".xstatus"))
openStatusDialog();
else if (this.classList.contains("visibility"))
Native.ToggleVisibility();
else
Native.SetStatus(parseInt(this.attr["code"]));
});
setMenuListener(divmenu, function() {
switch (this.id) {
case "mnewgroup": Native.AddGroup(); break;
case "maddcontact": openAddContactDialog(); break;
case "mexpandall": cl.toggleAll("expand"); break;
case "mcollapseall": cl.toggleAll("collapse"); break;
case "mdeleteempty": Native.DeleteEmptyGroups(); break;
case "mshowgroups": Native.ToggleShowGroups(); break;
case "mshowemptygroups": Native.ToggleShowEmptyGroups(); break;
case "monlineonly": Native.ToggleOnlineOnly(); break;
case "mseparation": Native.ToggleStatusSeparation(); break;
}
});
setMenuListener(groupmenu, function() {
var groupid = this.attr["groupid"];
if (this.parent)
switch (this.parent.id) {
case "mgrouplist":
if (groupid) Native.MoveAllFromCurrentToGroup(parseInt(groupid));
return;
}
switch (this.id) {
case "mnewgroup2": Native.AddGroup(); break;
case "maddcontact2": openAddContactDialog(); break;
case "maddgrouptoserver": Native.AddGroupToServer(); break;
case "mremovegroupfromserver": Native.RemoveCurrentGroupFromServer(); break;
case "mrenamegroup": cl.getCurrent().startEditing(); break;
case "mdeletegroup": Native.DeleteCurrentGroup(); break;
case "mshowgroups2": Native.ToggleShowGroups(); break;
}
});
var suppressUp = false;
document.on("^mousedown", () => { suppressUp = false; })
.on("^mouseup", () => { return suppressUp; })
.on("^dblclick", () => { suppressUp = true; })
.on("openmenu", (e) => {
if (e.data == 'main') openMainMenu();
else if (e.data == 'status') openStatusMenu();
});
view.on("replacementstart", (e) => {
windowResizing = true;
}).on("replacementend", (e) => {
windowResizing = false;
if (cl) cl.onsizechange();
}).on("activate", function(e) {
Native.OnCLActivate();
var active = e.reason !== 0;
if (!active) isModKey = false;
if (document.state.ownspopup || (active && view.focus != null)) return;
cl.post(() => { this.state.focus = active });
});
// openSingleWindow({
// url: "debug.htm",
// caption: "Debug",
// translate: false,
// uniqueid: "debug",
// alignment: 5,
// width: 450,
// height: 550
// });
</script>
</head>
<body>
<div id="bar">
<div class="imgbtn" id="menuBtn" title="Menu"><div pic="rnq"></div></div>
<div class="imgbtn" id="statusBtn" title="Status"><div pic="status.offline"></div></div>
<div class="imgbtn" id="addBtn"><div pic></div><div class="emoji"></div></div>
<div id="progress">
<progress id="pslider" value="0" />
<div id="pvalue"></div>
<div id="ccount"></div>
</div>
</div>
<widget id="cl" type="tree"></widget>
<div id="filter">
<input id="clfilter" />
<div class="imgbtn filterbtn" id="clearBtn" title="Clear filter text"><div pic="cancel"></div></div>
</div>
<menu id="mainmenu" class="context custom">
<li><div id="mstatus" pic="status.offline"></div><span>Status</span><menu id="mstatuses"></menu></li>
<li><div pic="key"></div><span>Security</span><menu>
<li id="mprotopassword"><div pic="key"></div><span>Login password</span></li>
<li id="maccpassword"><div pic="key"></div><span>Account password</span></li>
<li id="mlock"><div pic="key"></div><span>Lock</span></li>
</menu></li>
<li><div pic="special"></div><span>Special</span><menu>
<li id="mviewinfoabout"><div pic="info"></div><span>View info about...</span></li>
<li id="mopenchat"><div pic="msg"></div><span>Open chat with...</span></li>
<li id="msearch"><div pic="search"></div><span>History search</span></li>
<li id="moutbox"><div pic="outbox"></div><span>Outbox</span></li>
<li id="mdb"><div pic="db"></div><span>Contacts database</span></li>
<li id="mlog"><div pic="log"></div><span>Show log window</span></li>
<li id="mreloadlang"><div pic="refresh"></div><span>Reload current language</span></li>
<hr/>
<li id="mgetcl"><div pic="request.contact.list"></div><span>Load contact list from server</span></li>
<li id="msessionsmgr"><div pic="sessions"></div><span>Sessions manager</span></li>
</menu></li>
<li><div pic="theme"></div><span>Themes</span><menu id="mthemes">
<li id="mreloadtheme"><div pic="refresh"></div><span>Reload current theme</span></li>
<li id="mthemelist"><div pic="reload.theme.list"></div><span>Refresh theme-list</span></li>
<li id="mcontactthemes" title="Open contacts-theme in notepad"><div pic="theme"></div><span>Open contacts-theme</span></li>
<li id="msmiles"><div pic="smiles"></div><span>Smiles</span><menu id="msmileslist"></menu></li>
<li id="memojis"><div pic="emoji"></div><span>Emojis</span><menu id="memojislist"></menu></li>
<li id="msounds"><div pic="sounds"></div><span>Sounds</span><menu id="msoundslist"></menu></li>
<hr/>
</menu></li>
<li><div pic="support"></div><span>Support</span><menu>
<li id="mupdate"><div pic="download"></div><span>Check for updates</span></li>
<hr/>
<li id="mportal" title="https://rnq.ru"><div></div><span>R&Q Portal</span></li>
<li id="mhelp" title="https://help.rnq.ru"><div></div><span>R&Q Help</span></li>
</menu></li>
<li id="madduin"><div pic="uin"></div><span>Add contact</span></li>
<li id="musers"><div pic="users"></div><span>Switch user</span></li>
<li id="mprefs"><div pic="preferences"></div><span>Preferences</span></li>
<li id="mmyinfo"><div pic="info"></div><span>View my info</span></li>
<li id="mmyprofile"><div pic="webinfo"></div><span>View my web profile</span></li>
<hr/>
<li id="mabout"><div pic="rnq"></div><span>About R&Q</span></li>
<li id="mlocalhelp"><div pic="help"></div><span>Help</span></li>
<li id="mtoggle"><div pic="minimize"></div><span>Hide</span></li>
<li id="mexit"><div pic="quit"></div><span>Quit</span></li>
</menu>
<menu id="statusmenu" class="context custom"></menu>
<menu id="divmenu" class="context custom">
<li id="mnewgroup"><div pic="new.group"></div><span>New group</span></li>
<li id="maddcontact"><div pic="add.contact"></div><span>Add contact</span></li>
<hr/>
<li id="mexpandall"><div pic="open.group"></div><span>Open all groups</span></li>
<li id="mcollapseall"><div pic="close.group"></div><span>Close all groups</span></li>
<li id="mdeleteempty"><div pic="delete"></div><span>Delete all empty groups</span></li>
<hr/>
<li id="mshowgroups"><div pic></div><span>Show groups</span></li>
<li id="mshowemptygroups"><div pic></div><span>Show empty groups</span></li>
<li id="monlineonly"><div pic></div><span>Show only online contacts</span></li>
<li id="mseparation"><div pic></div><span>Not separate by online\offline</span></li>
</menu>
<menu id="groupmenu" class="context custom">
<li id="mnewgroup2"><div pic="new.group"></div><span>New group</span></li>
<li id="maddcontact2"><div pic="add.contact"></div><span>Add contact</span></li>
<li id="maddgrouptoserver"><div pic="add"></div><span>Add to server</span></li>
<li id="mremovegroupfromserver"><div pic="add"></div><span>Remove from server (keep local)</span></li>
<hr/>
<li id="mrenamegroup"><div pic="rename"></div><span>Rename group</span></li>
<li id="mdeletegroup"><div pic="delete"></div><span>Delete group</span></li>
<li id="mmoveallto"><div pic="close.group"></div><span>Move all contacts to</span><menu id="mgrouplist"></menu></li>
<hr/>
<li id="mshowgroups2"><div pic></div><span>Show groups</span></li>
</menu>
<include src="menus/contactmenu.htm" />
</body>
</html>