From be1ecf607d37ffb950bb432906bca4056b21e274 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Fri, 28 May 2021 11:45:27 -0400 Subject: [PATCH] Display prefixes in member list Closes: https://todo.sr.ht/~emersion/gamja/43 --- components/app.js | 84 +++++++++++++++++++++++++++++++++++++++ components/member-list.js | 54 +++++++++++++++++++++++-- lib/irc.js | 21 ++++++++++ style.css | 16 ++++++++ 4 files changed, 171 insertions(+), 4 deletions(-) diff --git a/components/app.js b/components/app.js index 149ddc4..f42aaf6 100644 --- a/components/app.js +++ b/components/app.js @@ -683,6 +683,7 @@ export default class App extends Component { if (this.isChannel(target)) { this.addMessage(netID, target, msg); } + this.handleMode(netID, msg); break; case "NOTICE": case "PRIVMSG": @@ -1218,6 +1219,89 @@ export default class App extends Component { this.setState({ dialog: null, networkDialog: null }); } + handleMode(netID, msg) { + var client = this.clients.get(netID); + var chanmodes = client.isupport.get("CHANMODES") || irc.STD_CHANMODES; + var prefix = client.isupport.get("PREFIX") || ""; + + var prefixByMode = new Map(irc.parseMemberships(prefix).map((membership) => { + return [membership.mode, membership.prefix]; + })); + + var typeByMode = new Map(); + var [a, b, c, d] = chanmodes.split(","); + Array.from(a).forEach((mode) => typeByMode.set(mode, "A")); + Array.from(b).forEach((mode) => typeByMode.set(mode, "B")); + Array.from(c).forEach((mode) => typeByMode.set(mode, "C")); + Array.from(d).forEach((mode) => typeByMode.set(mode, "D")); + prefixByMode.forEach((prefix, mode) => typeByMode.set(mode, "B")); + + var channel = msg.params[0]; + var change = msg.params[1]; + var args = msg.params.slice(2); + + var plusMinus = null; + var j = 0; + for (var i = 0; i < change.length; i++) { + if (change[i] === "+" || change[i] === "-") { + plusMinus = change[i]; + continue; + } + if (!plusMinus) { + throw new Error("malformed mode string: missing plus/minus"); + } + + var mode = change[i]; + var add = plusMinus === "+"; + + var modeType = typeByMode.get(mode); + if (!modeType) { + continue; + } + + var arg = null; + if (modeType === "A" || modeType === "B" || (modeType === "C" && add)) { + arg = args[j]; + j++; + } + + if (prefixByMode.has(mode)) { + this.handlePrefixChange(netID, channel, arg, prefixByMode.get(mode), add); + } + + // XXX: If we eventually want to handle any mode changes with + // some special logic, this would be the place to. Not sure + // what we'd want to do in that regard, though. + } + } + + handlePrefixChange(netID, channel, nick, letter, add) { + var client = this.clients.get(netID); + var prefix = client.isupport.get("PREFIX") || ""; + + var prefixPrivs = new Map(irc.parseMemberships(prefix).map((membership, i) => { + return [membership.prefix, i]; + })); + + this.setBufferState({ network: netID, name: channel }, (buf) => { + var members = new irc.CaseMapMap(buf.members); + var membership = members.get(nick); + if (add) { + var i = membership.indexOf(letter); + if (i < 0) { + membership += letter; + } + } else { + membership = membership.replace(letter, ""); + } + membership = Array.from(membership).sort((a, b) => { + return prefixPrivs.get(a) - prefixPrivs.get(b); + }).join(""); + members.set(nick, membership); + return { members }; + }); + } + componentDidMount() { setupKeybindings(this); } diff --git a/components/member-list.js b/components/member-list.js index 59f3efe..3926df5 100644 --- a/components/member-list.js +++ b/components/member-list.js @@ -9,7 +9,8 @@ class MemberItem extends Component { } shouldComponentUpdate(nextProps) { - return this.props.nick !== nextProps.nick; + return this.props.nick !== nextProps.nick + || this.props.membership != nextProps.membership; } handleClick(event) { @@ -18,14 +19,54 @@ class MemberItem extends Component { } render() { + // XXX: If we were feeling creative we could generate unique colors for + // each item in ISUPPORT CHANMODES. But I am not feeling creative. + const membmap = { + "~": "owner", + "&": "admin", + "@": "op", + "%": "halfop", + "+": "voice", + }; + const membclass = membmap[this.props.membership[0]] || ""; + let membership = ""; + if (this.props.membership) { + membership = html` + + ${this.props.membership} + + `; + }; return html`
  • - ${this.props.nick} + ${membership}${this.props.nick}
  • `; } } +function sortMembers(a, b) { + var [nickA, membA] = a, [nickB, membB] = b; + + const prefixPrivs = ["~", "&", "@", "%", "+"]; // TODO: grab it from ISUPPORT PREFIX + var i = prefixPrivs.indexOf(membA[0]), j = prefixPrivs.indexOf(membB[0]); + if (i < 0) { + i = prefixPrivs.length; + } + if (j < 0) { + j = prefixPrivs.length; + } + if (i !== j) { + return i - j; + } + + return nickA < nickB ? -1 : 1; +} + export default class MemberList extends Component { shouldComponentUpdate(nextProps) { return this.props.members !== nextProps.members; @@ -34,8 +75,13 @@ export default class MemberList extends Component { render() { return html` `; diff --git a/lib/irc.js b/lib/irc.js index 72eb832..fe9559a 100644 --- a/lib/irc.js +++ b/lib/irc.js @@ -41,6 +41,7 @@ export const ERR_SASLABORTED = "906"; export const ERR_SASLALREADY = "907"; export const STD_CHANNEL_TYPES = "#&+!"; +export const STD_CHANMODES = "beI,k,l,imnst"; const tagEscapeMap = { ";": "\\:", @@ -510,3 +511,23 @@ export class CaseMapMap { return this.entries(); } } + +export function parseMemberships(str) { + if (str[0] !== "(") { + throw new Error("malformed ISUPPORT PREFIX value: expected opening parenthesis"); + } + + var sep = str.indexOf(")"); + if (sep < 0) { + throw new Error("malformed ISUPPORT PREFIX value: expected closing parenthesis"); + } + + var n = str.length - sep - 1; + var memberships = []; + for (var i = 0; i < n; i++) { + var mode = str[i + 1]; + var prefix = str[sep + i + 1]; + memberships.push({ mode, prefix }); + } + return memberships; +} diff --git a/style.css b/style.css index 2f5ba36..3b8e312 100644 --- a/style.css +++ b/style.css @@ -261,6 +261,22 @@ button.danger:hover { box-sizing: border-box; } +.membership.owner { + color: red; +} +.membership.admin { + color: blue; +} +.membership.op { + color: var(--green); +} +.membership.halfop { + color: orange; +} +.membership.voice { + color: yellow; +} + #composer { color: var(--main-color); background: var(--main-background);