Merge branch 'friendlist' into 'master'

ENH: Use semantic HTML for the friend list and add a new column for room types

See merge request 
This commit is contained in:
BondageProjects 2025-03-25 00:10:55 +00:00
commit 4e79439d91
9 changed files with 311 additions and 123 deletions

View file

@ -33,6 +33,7 @@
padding: var(--small-gap);
width: 20%;
text-align: center;
user-select: none;
}
#friend-list-search-input {
@ -61,17 +62,19 @@
/* #endregion */
/* #region HEADER */
#friend-list-header {
min-height: var(--row-height);
display: flex;
align-items: center;
color: var(--text-color);
user-select: none;
}
#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%;
}
@ -105,6 +108,60 @@
#friend-list-beep-dialog:not([data-received]) .fl-beep-sent-content {
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);
width: var(--row-height);
max-width: 50px;
max-height: 50px;
margin-inline: 0.15em;
aspect-ratio: 1 / 1;
}
/* #endregion */
/* #region FRIENDLIST */
@ -124,17 +181,27 @@
width: calc(100% - var(--button-size));
}
.friend-list-row {
color: var(--text-color);
display: flex;
flex-direction: row;
justify-content: space-evenly;
#friend-list-table th {
font-weight: normal;
}
.friend-list-row * {
.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 > * {
vertical-align: middle;
text-justify: center;
padding: unset;
}
.friend-list-row:hover {
color: yellow;
}
@ -147,10 +214,6 @@
white-space: preserve;
}
#friend-list .friend-list-column {
height: 100%;
}
#friend-list-member-number,
.MemberNumber {
width: 10%;
@ -168,8 +231,8 @@
gap: var(--small-gap);
}
.RelationType img {
height: min(5dvh, 2.5dvw);
.RelationType {
user-select: none;
}
.friend-list-link {

Binary file not shown.

After

(image error) Size: 3.5 KiB

Binary file not shown.

After

(image error) Size: 2 KiB

Binary file not shown.

After

(image error) Size: 3.6 KiB

Binary file not shown.

After

(image error) Size: 2.1 KiB

View file

@ -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;
}

View file

@ -90,7 +90,7 @@ function FriendListLoad() {
tag: 'input',
attributes: {
id: FriendListIDs.searchInput,
type: 'text',
type: 'search',
maxLength: 100,
},
eventListeners: {
@ -198,71 +198,87 @@ 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-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"),
{ 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 +288,7 @@ function FriendListLoad() {
}
},
{
tag: 'div',
tag: 'tbody',
classList: ["scroll-box"],
attributes: {
id: FriendListIDs.friendList
@ -504,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
});
@ -537,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
@ -559,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");
@ -601,15 +625,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[]} */
@ -621,25 +658,20 @@ function FriendListLoadFriendList(data) {
const originalChatRoomName = friend.ChatRoomName || '';
const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${friend.ChatRoomSpace || "F"}`);
const chatRoomName = ChatSearchMuffle(friend.ChatRoomName?.replaceAll('<', '&lt;').replaceAll('>', '&gt;') || 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,
@ -653,18 +685,9 @@ function FriendListLoadFriendList(data) {
const beepData = FriendListBeepLog[i];
const chatRoomSpaceCaption = InterfaceTextGet(`ChatRoomSpace${beepData.ChatRoomSpace || "F"}`);
const chatRoomName = ChatSearchMuffle(beepData.ChatRoomName?.replaceAll('<', '&lt;').replaceAll('>', '&gt;') || 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);
@ -683,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,
@ -718,18 +745,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()
@ -740,20 +767,96 @@ function FriendListLoadFriendList(data) {
if (friend.chatRoom) {
if (!friend.chatRoom.name || !friend.chatRoom.canSearchRoom) {
row.appendChild(ElementCreate({
tag: "span",
classList: ['friend-list-column', 'ChatRoomName'],
innerHTML: friend.chatRoom.caption,
}));
// 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', 'ChatRoomName'],
children: [friend.chatRoom.caption],
style: { "user-select": friend.chatRoom.caption === "-" ? "none" : undefined },
}),
);
} 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),
},
}));
// 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,
style: { "user-select": friend.chatRoom.caption === "-" ? "none" : undefined },
eventListeners: {
click: () => FriendListChatSearch(friend.chatRoom.name),
},
}),
);
}
}
@ -767,6 +870,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 +880,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,14 +897,18 @@ 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: [
{
tag: 'img',
tag: "img",
attributes: {
src: relationTypeIcons[friend.relationType],
}
decoding: "async",
loading: "lazy",
"aria-hidden": "true",
},
classList: ["friend-list-icon-small"],
},
FriendTypeCaption[friend.relationType]
],
@ -813,7 +922,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" },
}}
));

View file

@ -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

1 OnlineFriends Online friends
5 MemberNickname Nickname
6 MemberNumber Member number
7 ChatRoomName Chat room
8 FriendType ChatRoomType Relation type Room type
9 RelationType Relation type
10 ActionFriends Send a Beep
11 ActionRead Read a Beep
12 ActionDelete Delete a Friend
22 TypeLover Lover
23 TypeSubmissive Submissive
24 TypeFriend Friend
25 TypeFemale Femaly-only room
26 TypeMale Male-only room
27 TypeMixed Mixed male/female room
28 TypeAsylum Asylum room
29 TypePrivate Private room

View file

@ -901,7 +901,7 @@ interface IFriendListBeepLogMessage {
MemberName: string;
ChatRoomName?: string;
Private: boolean;
ChatRoomSpace?: string;
ChatRoomSpace?: ServerChatRoomSpace;
Sent: boolean;
Time: Date;
Message?: string;