From 74b0ea5c4ad118201ec31bad3fc98a2f76f1fb26 Mon Sep 17 00:00:00 2001 From: bananarama92 <bananarama921@outlook.com> Date: Tue, 18 Mar 2025 20:37:41 +0100 Subject: [PATCH 1/6] ENH: Use a `<table>` element for the friend list f --- BondageClub/CSS/FriendList.css | 17 ++- .../Character/FriendList/FriendList.js | 134 ++++++++++-------- 2 files changed, 82 insertions(+), 69 deletions(-) diff --git a/BondageClub/CSS/FriendList.css b/BondageClub/CSS/FriendList.css index 5745a58e42..b2d850682a 100644 --- a/BondageClub/CSS/FriendList.css +++ b/BondageClub/CSS/FriendList.css @@ -61,17 +61,14 @@ /* #endregion */ /* #region HEADER */ -#friend-list-header { - min-height: var(--row-height); - display: flex; - align-items: center; - color: var(--text-color); -} - #friend-list-header .friend-list-link { text-decoration: none; } +#friend-list-header .friend-list-row:hover { + color: var(--text-color); +} + #friend-list-header-hr { width: 80%; } @@ -124,9 +121,15 @@ width: calc(100% - var(--button-size)); } +#friend-list-table td { + margin: unset; + font-weight: normal; +} + .friend-list-row { color: var(--text-color); display: flex; + align-items: center; flex-direction: row; justify-content: space-evenly; } diff --git a/BondageClub/Screens/Character/FriendList/FriendList.js b/BondageClub/Screens/Character/FriendList/FriendList.js index accc1b3117..cd9b0b97b3 100644 --- a/BondageClub/Screens/Character/FriendList/FriendList.js +++ b/BondageClub/Screens/Character/FriendList/FriendList.js @@ -90,7 +90,7 @@ function FriendListLoad() { tag: 'input', attributes: { id: FriendListIDs.searchInput, - type: 'text', + type: 'search', maxLength: 100, }, eventListeners: { @@ -198,71 +198,78 @@ function FriendListLoad() { } ), ElementCreate({ - tag: 'div', + tag: 'table', attributes: { - id: FriendListIDs.friendListTable + id: FriendListIDs.friendListTable, + "aria-labelledby": FriendListIDs.modeTitle, }, children: [ { - tag: 'div', + tag: 'thead', attributes: { id: FriendListIDs.header }, - classList: ['friend-list-row'], children: [ - ElementButton.Create( - "friend-list-member-name", - () => FriendListChangeSortingMode("MemberName"), - { noStyling: true }, - { button: { - classList: ['friend-list-column', 'friend-list-link'], - }}, - ), - ElementButton.Create( - "friend-list-member-number", - () => FriendListChangeSortingMode("MemberNumber"), - { noStyling: true }, - { button: { - classList: ['friend-list-column', 'friend-list-link'], - }}, - ), - ElementButton.Create( - "friend-list-chat-room-name", - () => FriendListChangeSortingMode("ChatRoomName"), - { noStyling: true }, - { button: { - classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content', 'fl-beeps-content'], - }}, - ), - ElementButton.Create( - "friend-list-relation-type", - () => FriendListChangeSortingMode("RelationType"), - { noStyling: true }, - { button: { - classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'], - }}, - ), { - tag: "span", - classList: ['friend-list-column', 'mode-specific-content', 'fl-online-friends-content'], + tag: "tr", + classList: ["friend-list-row"], children: [ - TextGet("ActionFriends") + ElementButton.Create( + "friend-list-member-name", + () => FriendListChangeSortingMode("MemberName"), + { noStyling: true }, + { button: { + classList: ['friend-list-column', 'friend-list-link'], + attributes: { role: "columnheader" }, + }}, + ), + ElementButton.Create( + "friend-list-member-number", + () => FriendListChangeSortingMode("MemberNumber"), + { noStyling: true }, + { button: { + classList: ['friend-list-column', 'friend-list-link'], + attributes: { role: "columnheader" }, + }}, + ), + ElementButton.Create( + "friend-list-chat-room-name", + () => FriendListChangeSortingMode("ChatRoomName"), + { noStyling: true }, + { button: { + classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content', 'fl-beeps-content'], + attributes: { role: "columnheader" }, + }}, + ), + ElementButton.Create( + "friend-list-relation-type", + () => FriendListChangeSortingMode("RelationType"), + { noStyling: true }, + { button: { + classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'], + attributes: { role: "columnheader" }, + }}, + ), + { + tag: "th", + classList: ['friend-list-column', 'mode-specific-content', 'fl-online-friends-content'], + attributes: { scope: "col" }, + children: [TextGet("ActionFriends")], + }, + { + tag: "th", + classList: ['friend-list-column', 'mode-specific-content', 'fl-beeps-content'], + attributes: { scope: "col" }, + children: [TextGet("ActionRead")], + }, + { + tag: "th", + classList: ['friend-list-column', 'mode-specific-content', 'fl-all-friends-content'], + attributes: { scope: "col" }, + children: [TextGet("ActionDelete")], + }, ], }, - { - tag: "span", - classList: ['friend-list-column', 'mode-specific-content', 'fl-beeps-content'], - children: [ - TextGet("ActionRead") - ], - }, - { - tag: "span", - classList: ['friend-list-column', 'mode-specific-content', 'fl-all-friends-content'], - children: [ - TextGet("ActionDelete") - ], - } ] }, { @@ -272,7 +279,7 @@ function FriendListLoad() { } }, { - tag: 'div', + tag: 'tbody', classList: ["scroll-box"], attributes: { id: FriendListIDs.friendList @@ -718,18 +725,18 @@ function FriendListLoadFriendList(data) { friendRawData.forEach(friend => { const row = ElementCreate({ - tag: "div", + tag: "tr", classList: ['friend-list-row'], children: [ { - tag: "span", + tag: "td", classList: ['friend-list-column', 'MemberName'], children: [ friend.memberName ], }, { - tag: "span", + tag: "td", classList: ['friend-list-column', 'MemberNumber'], children: [ friend.memberNumber.toString() @@ -741,7 +748,7 @@ function FriendListLoadFriendList(data) { if (friend.chatRoom) { if (!friend.chatRoom.name || !friend.chatRoom.canSearchRoom) { row.appendChild(ElementCreate({ - tag: "span", + tag: "td", classList: ['friend-list-column', 'ChatRoomName'], innerHTML: friend.chatRoom.caption, })); @@ -753,6 +760,7 @@ function FriendListLoadFriendList(data) { eventListeners: { click: () => FriendListChatSearch(friend.chatRoom.name), }, + attributes: { role: "cell" }, })); } } @@ -767,6 +775,7 @@ function FriendListLoadFriendList(data) { { button: { classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content'], children: [friend.beep.caption], + attributes: { role: "cell" }, }}, ), ElementButton.Create( @@ -776,12 +785,13 @@ function FriendListLoadFriendList(data) { { button: { classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-beeps-content'], children: [friend.beep.caption], + attributes: { role: "cell" }, }}, ), ); } else { row.appendChild(ElementCreate({ - tag: "span", + tag: "td", classList: ['friend-list-column'], children: [ friend.beep.caption @@ -792,7 +802,7 @@ function FriendListLoadFriendList(data) { if (friend.relationType) { row.appendChild(ElementCreate({ - tag: "span", + tag: "td", classList: ['friend-list-column', 'RelationType', 'mode-specific-content', 'fl-all-friends-content'], children: [ { @@ -813,7 +823,7 @@ function FriendListLoadFriendList(data) { { button: { classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-all-friends-content'], children: [FriendListConfirmDelete.includes(friend.memberNumber) ? ConfirmDeleteCaption : DeleteCaption], - attributes: { disabled: !friend.canDelete }, + attributes: { disabled: !friend.canDelete, role: "cell" }, }} )); From 1bac6b223df482e10596ca0aecb85fc9f7b14533 Mon Sep 17 00:00:00 2001 From: bananarama92 <bananarama921@outlook.com> Date: Tue, 18 Mar 2025 20:38:21 +0100 Subject: [PATCH 2/6] ENH: Set the `aria-sort` attribute on friendlist table headers --- .../Character/FriendList/FriendList.js | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/BondageClub/Screens/Character/FriendList/FriendList.js b/BondageClub/Screens/Character/FriendList/FriendList.js index cd9b0b97b3..2ab7c105cc 100644 --- a/BondageClub/Screens/Character/FriendList/FriendList.js +++ b/BondageClub/Screens/Character/FriendList/FriendList.js @@ -608,15 +608,28 @@ function FriendListLoadFriendList(data) { }); if (infoChanged) ServerPlayerRelationsSync(); + /** @satisfies {Record<string, FriendListSortingMode>} */ const columnHeaders = { - "friend-list-member-name": `${TextGet("MemberName")} ${FriendListSortingMode === "MemberName" ? sortingSymbol : "↕"}`, - "friend-list-member-number": `${TextGet("MemberNumber")} ${FriendListSortingMode === "MemberNumber" ? sortingSymbol : "↕"}`, - "friend-list-chat-room-name": `${TextGet("ChatRoomName")} ${FriendListSortingMode === "ChatRoomName" ? sortingSymbol : "↕"}`, - "friend-list-relation-type": `${TextGet("FriendType")} ${FriendListSortingMode === "RelationType" ? sortingSymbol : "↕"}`, + "friend-list-member-name": "MemberName", + "friend-list-member-number": "MemberNumber", + "friend-list-chat-room-name": "ChatRoomName", + "friend-list-chat-room-type": "ChatRoomType", + "friend-list-relation-type": "RelationType", }; - CommonEntries(columnHeaders).forEach(([id, textContent]) => { + CommonEntries(columnHeaders).forEach(([id, modeName]) => { const elem = document.getElementById(id); - elem.textContent = textContent; + const elemSortingSymbol = FriendListSortingMode === modeName ? sortingSymbol : "↕"; + elem.textContent = `${TextGet(modeName)} ${elemSortingSymbol}`; + switch (elemSortingSymbol) { + case "↑": + elem.setAttribute("aria-sort", "ascending"); + break; + case "↓": + elem.setAttribute("aria-sort", "descending"); + break; + default: + elem.setAttribute("aria-sort", "none"); + } }); /** @type {FriendRawData[]} */ From 0b2bb53c8d36d660d8ce766e91401289d2c1803d Mon Sep 17 00:00:00 2001 From: bananarama92 <bananarama921@outlook.com> Date: Tue, 18 Mar 2025 20:42:06 +0100 Subject: [PATCH 3/6] MAINT: Standardize the friend list icon HTML and CSS --- BondageClub/CSS/FriendList.css | 15 +++++++++++---- .../Screens/Character/FriendList/FriendList.js | 8 ++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/BondageClub/CSS/FriendList.css b/BondageClub/CSS/FriendList.css index b2d850682a..0bfe3a14dd 100644 --- a/BondageClub/CSS/FriendList.css +++ b/BondageClub/CSS/FriendList.css @@ -102,6 +102,17 @@ #friend-list-beep-dialog:not([data-received]) .fl-beep-sent-content { display: block; } + +.friend-list-icon-small { + pointer-events: none; + height: var(--row-height); + width: var(--row-height); + max-width: 50px; + max-height: 50px; + margin-inline: 0.15em; + aspect-ratio: 1 / 1; +} + /* #endregion */ /* #region FRIENDLIST */ @@ -171,10 +182,6 @@ gap: var(--small-gap); } -.RelationType img { - height: min(5dvh, 2.5dvw); -} - .friend-list-link { text-decoration: underline; cursor: pointer; diff --git a/BondageClub/Screens/Character/FriendList/FriendList.js b/BondageClub/Screens/Character/FriendList/FriendList.js index 2ab7c105cc..86264dae8b 100644 --- a/BondageClub/Screens/Character/FriendList/FriendList.js +++ b/BondageClub/Screens/Character/FriendList/FriendList.js @@ -819,10 +819,14 @@ function FriendListLoadFriendList(data) { classList: ['friend-list-column', 'RelationType', 'mode-specific-content', 'fl-all-friends-content'], children: [ { - tag: 'img', + tag: "img", attributes: { src: relationTypeIcons[friend.relationType], - } + decoding: "async", + loading: "lazy", + "aria-hidden": "true", + }, + classList: ["friend-list-icon-small"], }, FriendTypeCaption[friend.relationType] ], From 620bbd13943c57c6ac0c66d0c6ea1d5f402e6c2f Mon Sep 17 00:00:00 2001 From: bananarama92 <bananarama921@outlook.com> Date: Tue, 18 Mar 2025 20:38:28 +0100 Subject: [PATCH 4/6] ENH: Add a friend list column with room type info --- BondageClub/CSS/FriendList.css | 59 ++++++- BondageClub/Icons/FemaleInverted.png | Bin 0 -> 3626 bytes BondageClub/Icons/GenderInvert.png | Bin 0 -> 2054 bytes BondageClub/Icons/MaleInverted.png | Bin 0 -> 3710 bytes BondageClub/Icons/PrivateInvert.png | Bin 0 -> 2172 bytes .../Character/FriendList/FriendList.d.ts | 12 +- .../Character/FriendList/FriendList.js | 148 ++++++++++++++---- .../Character/FriendList/Text_FriendList.csv | 8 +- BondageClub/Scripts/Typedef.d.ts | 2 +- 9 files changed, 184 insertions(+), 45 deletions(-) create mode 100644 BondageClub/Icons/FemaleInverted.png create mode 100644 BondageClub/Icons/GenderInvert.png create mode 100644 BondageClub/Icons/MaleInverted.png create mode 100644 BondageClub/Icons/PrivateInvert.png diff --git a/BondageClub/CSS/FriendList.css b/BondageClub/CSS/FriendList.css index 0bfe3a14dd..92a87747a1 100644 --- a/BondageClub/CSS/FriendList.css +++ b/BondageClub/CSS/FriendList.css @@ -103,6 +103,49 @@ display: block; } +.ChatRoomType { + display: flex; + justify-content: center; + gap: 0.15em; + user-select: none; +} + +.friend-list-icon-container { + position: relative; + height: var(--row-height); + width: var(--row-height); + max-width: 86px; + max-height: 86px; + aspect-ratio: 1 / 1; +} + +.friend-list-icon-container > .button-tooltip { + --tooltip-gap: 0.15em; +} + +.friend-list-icon { + pointer-events: none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +@media (hover: hover) { + @supports selector(:has(*)) { + .friend-list-icon-container:hover:not(:has(.button-tooltip:hover)) > .button-tooltip { + visibility: visible; + } + } + + @supports not selector(:has(*)) { + .friend-list-icon-container:hover > .button-tooltip { + visibility: visible; + } + } +} + .friend-list-icon-small { pointer-events: none; height: var(--row-height); @@ -132,21 +175,25 @@ width: calc(100% - var(--button-size)); } -#friend-list-table td { - margin: unset; +#friend-list-table th { font-weight: normal; } .friend-list-row { color: var(--text-color); + min-height: var(--row-height); display: flex; align-items: center; flex-direction: row; justify-content: space-evenly; + padding: unset; + margin: var(--small-gap) 0; } -.friend-list-row * { - margin: var(--small-gap) 0; +.friend-list-row > * { + vertical-align: middle; + text-justify: center; + padding: unset; } .friend-list-row:hover { @@ -161,10 +208,6 @@ white-space: preserve; } -#friend-list .friend-list-column { - height: 100%; -} - #friend-list-member-number, .MemberNumber { width: 10%; diff --git a/BondageClub/Icons/FemaleInverted.png b/BondageClub/Icons/FemaleInverted.png new file mode 100644 index 0000000000000000000000000000000000000000..1d89fee7c7eb762122498af11712370a21a8f19a GIT binary patch literal 3626 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 S_?`Mc4nCeMIj7t=#s3Ew{;miB literal 0 HcmV?d00001 diff --git a/BondageClub/Icons/GenderInvert.png b/BondageClub/Icons/GenderInvert.png new file mode 100644 index 0000000000000000000000000000000000000000..60a4145fc94b9413d5eebebc7fc9ddadfa6ae8c0 GIT binary patch literal 2054 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 literal 0 HcmV?d00001 diff --git a/BondageClub/Icons/MaleInverted.png b/BondageClub/Icons/MaleInverted.png new file mode 100644 index 0000000000000000000000000000000000000000..f32bddc6b9ce506b4611253eda28233f4f63a021 GIT binary patch literal 3710 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 literal 0 HcmV?d00001 diff --git a/BondageClub/Icons/PrivateInvert.png b/BondageClub/Icons/PrivateInvert.png new file mode 100644 index 0000000000000000000000000000000000000000..aea74c9ba4b487f28824f8adf35121541d53c98b GIT binary patch literal 2172 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 literal 0 HcmV?d00001 diff --git a/BondageClub/Screens/Character/FriendList/FriendList.d.ts b/BondageClub/Screens/Character/FriendList/FriendList.d.ts index dc71913876..f6ef4e1525 100644 --- a/BondageClub/Screens/Character/FriendList/FriendList.d.ts +++ b/BondageClub/Screens/Character/FriendList/FriendList.d.ts @@ -1,6 +1,6 @@ type FriendListModes = FriendListMode[]; type FriendListMode = "OnlineFriends" | "Beeps" | "AllFriends"; -type FriendListSortingMode = 'None' | 'MemberName' | 'MemberNickname' | 'MemberNumber' | 'ChatRoomName' | 'RelationType'; +type FriendListSortingMode = 'None' | 'MemberName' | 'MemberNickname' | 'MemberNumber' | 'ChatRoomName' | 'RelationType' | 'ChatRoomType'; type FriendListSortingDirection = 'Asc' | 'Desc'; type FriendListReturn<T extends ModuleType> = { Screen: ModuleScreens[T], Module: T, IsInChatRoom?: boolean, hasScrolledChat?: boolean }; @@ -19,6 +19,7 @@ type FriendRawRoom = { name?: string; caption: string; canSearchRoom: boolean; + types: FriendListIcon[]; }; type FriendRawBeep = { @@ -27,3 +28,12 @@ type FriendRawBeep = { hasMessage?: boolean; canBeep?: boolean; }; + +interface FriendListIcon { + /** The {@link HTMLImageElement.src} of the icon */ + src: string; + /** The `Character/FriendList` {@link TextGet} key of the icon's tooltip */ + tooltipKey: string; + /** A string to-be used for sorting the icon-containing column cells */ + sortKey: string; +} diff --git a/BondageClub/Screens/Character/FriendList/FriendList.js b/BondageClub/Screens/Character/FriendList/FriendList.js index 86264dae8b..bf7aecbeae 100644 --- a/BondageClub/Screens/Character/FriendList/FriendList.js +++ b/BondageClub/Screens/Character/FriendList/FriendList.js @@ -232,6 +232,15 @@ function FriendListLoad() { attributes: { role: "columnheader" }, }}, ), + ElementButton.Create( + "friend-list-chat-room-type", + () => FriendListChangeSortingMode("ChatRoomType"), + { noStyling: true }, + { button: { + classList: ['friend-list-column', 'friend-list-link', 'mode-specific-content', 'fl-online-friends-content', 'fl-beeps-content'], + attributes: { role: "columnheader" }, + }}, + ), ElementButton.Create( "friend-list-chat-room-name", () => FriendListChangeSortingMode("ChatRoomName"), @@ -544,6 +553,15 @@ function FriendListChatSearch(room) { ElementValue("InputSearch", ChatSearchMuffle(room)); } +/** @satisfies {{ [key in (ServerChatRoomSpace | "Private")]: FriendListIcon }} */ +const FriendListIconMapping = { + "": { src: "./Icons/FemaleInvert.png", tooltipKey: "TypeFemale", sortKey: "F " }, + M: { src: "./Icons/MaleInvert.png", tooltipKey: "TypeMale", sortKey: "M " }, + X: { src: "./Icons/GenderInvert.png", tooltipKey: "TypeMixed", sortKey: "FM" }, + Asylum: { src: "./Icons/Asylum.png", tooltipKey: "TypeAsylum", sortKey: "A " }, + Private: { src: "./Icons/PrivateInvert.png", tooltipKey: "TypePrivate", sortKey: "P" }, +}; + /** * Loads the friend list data into the HTML div element. * @param {ServerFriendInfo[]} data - An array of data, we receive from the server @@ -566,7 +584,6 @@ function FriendListLoadFriendList(data) { const BeepCaption = InterfaceTextGet("Beep"); const DeleteCaption = InterfaceTextGet("Delete"); const ConfirmDeleteCaption = InterfaceTextGet("ConfirmDelete"); - const PrivateRoomCaption = InterfaceTextGet("PrivateRoom"); const SentCaption = InterfaceTextGet("SentBeep"); const ReceivedCaption = InterfaceTextGet("ReceivedBeep"); const MailCaption = InterfaceTextGet("BeepWithMail"); @@ -641,25 +658,20 @@ function FriendListLoadFriendList(data) { const originalChatRoomName = friend.ChatRoomName || ''; const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${friend.ChatRoomSpace || "F"}`); const chatRoomName = ChatSearchMuffle(friend.ChatRoomName?.replaceAll('<', '<').replaceAll('>', '>') || undefined); - let caption = ''; const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatRoomSpace === (friend.ChatRoomSpace || ''); const canBeep = true; - const rawCaption = []; - if (chatRoomSpaceCaption && chatRoomName) rawCaption.push(`<i>${chatRoomSpaceCaption}</i>`); - if (friend.Private) rawCaption.push(PrivateRoomCaption); - if (chatRoomName) rawCaption.push(chatRoomName); - if (rawCaption.length === 0) rawCaption.push('-'); - - caption = rawCaption.join(' - '); - friendRawData.push({ memberName: friend.MemberName, memberNumber: friend.MemberNumber, chatRoom: { name: originalChatRoomName, - caption: caption, + caption: chatRoomName || "-", canSearchRoom: canSearchRoom, + types: [ + chatRoomSpaceCaption && chatRoomName ? FriendListIconMapping[friend.ChatRoomSpace ?? ""] : null, + friend.Private ? FriendListIconMapping.Private : null, + ].filter(Boolean), }, beep: { canBeep: canBeep, @@ -673,18 +685,9 @@ function FriendListLoadFriendList(data) { const beepData = FriendListBeepLog[i]; const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${beepData.ChatRoomSpace || "F"}`); const chatRoomName = ChatSearchMuffle(beepData.ChatRoomName?.replaceAll('<', '<').replaceAll('>', '>') || undefined); - let chatRoomCaption = ''; let beepCaption = ''; const canSearchRoom = FriendListReturn?.Screen === 'ChatSearch' && ChatRoomSpace === (beepData.ChatRoomSpace || ''); - const rawRoomCaption = []; - if (chatRoomSpaceCaption && chatRoomName) rawRoomCaption.push(`<i>${chatRoomSpaceCaption}</i>`); - if (beepData.Private) rawRoomCaption.push(PrivateRoomCaption); - if (chatRoomName) rawRoomCaption.push(chatRoomName); - if (rawRoomCaption.length === 0) rawRoomCaption.push('-'); - - chatRoomCaption = rawRoomCaption.join(' - '); - const rawBeepCaption = []; if (beepData.Sent) { rawBeepCaption.push(SentCaption); @@ -703,8 +706,12 @@ function FriendListLoadFriendList(data) { memberNumber: beepData.MemberNumber, chatRoom: { name: beepData.ChatRoomName, - caption: chatRoomCaption, + caption: chatRoomName || "-", canSearchRoom: canSearchRoom, + types: [ + chatRoomSpaceCaption && chatRoomName ? FriendListIconMapping[beepData.ChatRoomSpace ?? ""] : null, + beepData.Private ? FriendListIconMapping.Private : null, + ].filter(Boolean), }, beep: { beepIndex: i, @@ -760,21 +767,94 @@ function FriendListLoadFriendList(data) { if (friend.chatRoom) { if (!friend.chatRoom.name || !friend.chatRoom.canSearchRoom) { - row.appendChild(ElementCreate({ + // Sorting is performed via each cell's `textContent`, + // so explicitly prepend an invisible node with some sorting key + let totalSortKey = ""; + const imgContainer = ElementCreate({ tag: "td", - classList: ['friend-list-column', 'ChatRoomName'], - innerHTML: friend.chatRoom.caption, - })); + classList: ['friend-list-column', 'ChatRoomType'], + children: [ + { tag: "span", style: { display: "none" }, classList: ["friend-list-sorting-node"] }, + ...friend.chatRoom.types.map(({ src, tooltipKey, sortKey }) => { + totalSortKey += sortKey; + return { + tag: /** @type {const} */("div"), + classList: ["friend-list-icon-container"], + children: [ + { + tag: /** @type {const} */("img"), + attributes: { src, decoding: "async", loading: "lazy", alt: TextGet(tooltipKey) }, + classList: ["friend-list-icon"], + }, + { + tag: /** @type {const} */("div"), + attributes: { role: "tooltip", "aria-hidden": "true" }, + children: [TextGet(tooltipKey)], + classList: ["button-tooltip", "button-tooltip-right"], + }, + ], + }; + }), + ], + }); + imgContainer.children[0].textContent = totalSortKey + " "; + if (imgContainer.children.length === 1) { + imgContainer.append("-"); + } + row.append( + imgContainer, + ElementCreate({ + tag: "td", + classList: ['friend-list-column', 'ChatRoomName'], + children: [friend.chatRoom.caption], + }), + ); } else if (friend.chatRoom.canSearchRoom) { - row.appendChild(ElementCreate({ - tag: "button", - classList: ['friend-list-column', 'friend-list-link', 'blank-button', 'ChatRoomName'], - innerHTML: friend.chatRoom.caption, - eventListeners: { - click: () => FriendListChatSearch(friend.chatRoom.name), - }, - attributes: { role: "cell" }, - })); + // Sorting is performed via each cell's `textContent`, + // so explicitly prepend an invisible node with some sorting key + let totalSortKey = ""; + const imgContainer = ElementCreate({ + tag: "td", + classList: ['friend-list-column', 'ChatRoomType'], + children: [ + { tag: "span", style: { display: "none" }, classList: ["friend-list-sorting-node"] }, + ...friend.chatRoom.types.map(({ src, tooltipKey, sortKey }) => { + totalSortKey += sortKey; + return { + tag: /** @type {const} */("div"), + classList: ["friend-list-icon-container"], + children: [ + { + tag: /** @type {const} */("img"), + attributes: { src, decoding: "async", loading: "lazy", alt: TextGet(tooltipKey) }, + classList: ["friend-list-icon"], + }, + { + tag: /** @type {const} */("div"), + attributes: { role: "tooltip", "aria-hidden": "true" }, + children: [TextGet(tooltipKey)], + classList: ["button-tooltip", "button-tooltip-right"], + }, + ], + }; + }), + ], + }); + imgContainer.children[0].textContent = totalSortKey + " "; + if (imgContainer.children.length === 1) { + imgContainer.append("-"); + } + row.append( + imgContainer, + ElementCreate({ + tag: "td", + classList: ['friend-list-column', 'friend-list-link', 'blank-button', 'ChatRoomName'], + innerHTML: friend.chatRoom.caption, + eventListeners: { + click: () => FriendListChatSearch(friend.chatRoom.name), + }, + }), + ); } } diff --git a/BondageClub/Screens/Character/FriendList/Text_FriendList.csv b/BondageClub/Screens/Character/FriendList/Text_FriendList.csv index 4dbfda8561..74eb3b78ce 100644 --- a/BondageClub/Screens/Character/FriendList/Text_FriendList.csv +++ b/BondageClub/Screens/Character/FriendList/Text_FriendList.csv @@ -5,7 +5,8 @@ MemberName,Name MemberNickname,Nickname MemberNumber,Member number ChatRoomName,Chat room -FriendType,Relation type +ChatRoomType,Room type +RelationType,Relation type ActionFriends,Send a Beep ActionRead,Read a Beep ActionDelete,Delete a Friend @@ -21,3 +22,8 @@ TypeOwner,Owner TypeLover,Lover TypeSubmissive,Submissive TypeFriend,Friend +TypeFemale,Femaly-only room +TypeMale,Male-only room +TypeMixed,Mixed male/female room +TypeAsylum,Asylum room +TypePrivate,Private room diff --git a/BondageClub/Scripts/Typedef.d.ts b/BondageClub/Scripts/Typedef.d.ts index 4445bc80a4..d166455a90 100644 --- a/BondageClub/Scripts/Typedef.d.ts +++ b/BondageClub/Scripts/Typedef.d.ts @@ -881,7 +881,7 @@ interface IFriendListBeepLogMessage { MemberName: string; ChatRoomName?: string; Private: boolean; - ChatRoomSpace?: string; + ChatRoomSpace?: ServerChatRoomSpace; Sent: boolean; Time: Date; Message?: string; From 781405216ffa86e0edc6b1ff87bfb1d42dd31342 Mon Sep 17 00:00:00 2001 From: bananarama92 <bananarama921@outlook.com> Date: Tue, 18 Mar 2025 22:37:23 +0100 Subject: [PATCH 5/6] BUG: Fix the friend list beep menu always denoting your own room as non-private when sending a message --- BondageClub/Screens/Character/FriendList/FriendList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BondageClub/Screens/Character/FriendList/FriendList.js b/BondageClub/Screens/Character/FriendList/FriendList.js index bf7aecbeae..34c12167f5 100644 --- a/BondageClub/Screens/Character/FriendList/FriendList.js +++ b/BondageClub/Screens/Character/FriendList/FriendList.js @@ -520,7 +520,7 @@ function FriendListBeepMenuSend() { ChatRoomName: FriendListBeepShowRoom ? ChatRoomData?.Name : undefined, ChatRoomSpace: FriendListBeepShowRoom ? ChatRoomData?.Space : undefined, Sent: true, - Private: false, + Private: FriendListBeepShowRoom ? !ChatRoomData?.Visibility.includes("All") : undefined, Time: new Date(), Message: msg || undefined }); From 0d0086190fd1c2ec778b63a8b28ea0b22affc3ee Mon Sep 17 00:00:00 2001 From: bananarama92 <bananarama921@outlook.com> Date: Tue, 18 Mar 2025 22:57:16 +0100 Subject: [PATCH 6/6] MAINT: Ensure that friend list text with no relevant copyable content cannot be selected --- BondageClub/CSS/FriendList.css | 10 ++++++++++ BondageClub/Screens/Character/FriendList/FriendList.js | 2 ++ 2 files changed, 12 insertions(+) diff --git a/BondageClub/CSS/FriendList.css b/BondageClub/CSS/FriendList.css index 92a87747a1..1029007321 100644 --- a/BondageClub/CSS/FriendList.css +++ b/BondageClub/CSS/FriendList.css @@ -33,6 +33,7 @@ padding: var(--small-gap); width: 20%; text-align: center; + user-select: none; } #friend-list-search-input { @@ -61,6 +62,11 @@ /* #endregion */ /* #region HEADER */ + +#friend-list-header { + user-select: none; +} + #friend-list-header .friend-list-link { text-decoration: none; } @@ -225,6 +231,10 @@ gap: var(--small-gap); } +.RelationType { + user-select: none; +} + .friend-list-link { text-decoration: underline; cursor: pointer; diff --git a/BondageClub/Screens/Character/FriendList/FriendList.js b/BondageClub/Screens/Character/FriendList/FriendList.js index 34c12167f5..a4db0c57dd 100644 --- a/BondageClub/Screens/Character/FriendList/FriendList.js +++ b/BondageClub/Screens/Character/FriendList/FriendList.js @@ -807,6 +807,7 @@ function FriendListLoadFriendList(data) { tag: "td", classList: ['friend-list-column', 'ChatRoomName'], children: [friend.chatRoom.caption], + style: { "user-select": friend.chatRoom.caption === "-" ? "none" : undefined }, }), ); } else if (friend.chatRoom.canSearchRoom) { @@ -850,6 +851,7 @@ function FriendListLoadFriendList(data) { tag: "td", classList: ['friend-list-column', 'friend-list-link', 'blank-button', 'ChatRoomName'], innerHTML: friend.chatRoom.caption, + style: { "user-select": friend.chatRoom.caption === "-" ? "none" : undefined }, eventListeners: { click: () => FriendListChatSearch(friend.chatRoom.name), },