Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions src/functions/instantMessenger.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { displayText } from "../util/localization";
import { debug } from "../util/logger";
import { SDK, HOOK_PRIORITIES } from "../util/modding";
import { createPositionableButton, WCE_NAMESPACE } from "../util/publicApi";
import { fbcSettings } from "../util/settings";
import { parseJSON, objEntries, isNonNullObject, isString, fbcNotify } from "../util/utils";
import { registerSocketListener } from "./appendSocketListenersToInit";
Expand Down Expand Up @@ -221,7 +222,9 @@
friend.history.appendChild(divider);
}

if (container.classList.contains("bce-hidden")) {
// A visually hidden button is commonly covered by another addon's button. Do not retain a
// red unread state that suddenly appears when WCE's own button is shown again.
if (container.classList.contains("bce-hidden") && !isButtonHidden() && !isButtonVisualHidden()) {
unreadSinceOpened++;
}
}
Expand Down Expand Up @@ -279,7 +282,7 @@
return msgs;
}

// ToDo: migrate to IndexedDB

Check warning on line 285 in src/functions/instantMessenger.js

View workflow job for this annotation

GitHub Actions / lint

eslint(no-warning-comments)

src/functions/instantMessenger.js:285:3: Unexpected 'todo' comment: ToDo: migrate to IndexedDB
function loadIM() {
IMloaded = true;
const history = /** @type {Record<string, {historyRaw: RawHistory[]}>} */ (parseJSON(localStorage.getItem(storageKey()) || "{}"));
Expand Down Expand Up @@ -446,27 +449,52 @@
return next(args);
});

/** @type {[number, number, number, number]} */
const buttonPosition = [70, 905, 60, 60];
const DEFAULT_MESSENGER_Z_INDEX = 100;
const {
api: messengerButtonApi,
getPosition: getButtonPosition,
isHidden: isButtonHidden,
isVisualHidden: isButtonVisualHidden,
} = createPositionableButton([70, 905, 60, 60]);
WCE_NAMESPACE.Button = {
...WCE_NAMESPACE.Button,
Messenger: {
...messengerButtonApi,
isEnabled: () => fbcSettings.instantMessenger,
getZIndex: () => {
const zIndex = Number(container.style.zIndex || getComputedStyle(container).zIndex);
return Number.isFinite(zIndex) ? zIndex : DEFAULT_MESSENGER_Z_INDEX;
},
setZIndex: zIndex => {
if (typeof zIndex !== "number" || !Number.isFinite(zIndex)) {
throw new TypeError("setZIndex: zIndex must be a finite number");
}
container.style.zIndex = String(zIndex);
},
resetZIndex: () => {
container.style.zIndex = String(DEFAULT_MESSENGER_Z_INDEX);
},
},
};

SDK.hookFunction("DrawProcess", HOOK_PRIORITIES.AddBehaviour, (args, next) => {
const ret = next(args);
if (fbcSettings.instantMessenger) {
if (fbcSettings.instantMessenger && !isButtonHidden() && !isButtonVisualHidden()) {
if (
!fbcSettings.allowIMBypassBCX &&
(BCXgetRuleState("speech_restrict_beep_receive")?.isEnforced || (BCXgetRuleState("alt_hide_friends")?.isEnforced && Player.GetBlindLevel() >= 3))
) {
if (!container.classList.contains("bce-hidden")) hideIM();
DrawButton(...buttonPosition, "", "Gray", "Icons/Small/Chat.png", displayText("Instant Messenger (Disabled by BCX)"), false);
DrawButton(...getButtonPosition(), "", "Gray", "Icons/Small/Chat.png", displayText("Instant Messenger (Disabled by BCX)"), false);
} else {
DrawButton(...buttonPosition, "", unreadSinceOpened ? "Red" : "White", "Icons/Small/Chat.png", displayText("Instant Messenger"), false);
DrawButton(...getButtonPosition(), "", unreadSinceOpened ? "Red" : "White", "Icons/Small/Chat.png", displayText("Instant Messenger"), false);
}
}
return ret;
});

SDK.hookFunction("CommonClick", HOOK_PRIORITIES.OverrideBehaviour, (args, next) => {
if (fbcSettings.instantMessenger && MouseIn(...buttonPosition)) {
if (fbcSettings.instantMessenger && !isButtonHidden() && MouseIn(...getButtonPosition())) {
if (!container.classList.contains("bce-hidden")) {
hideIM();
return null;
Expand Down
48 changes: 46 additions & 2 deletions src/functions/pastProfiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { displayText } from "../util/localization";
import { debug, logInfo, logWarn, logError } from "../util/logger";
import { SDK, HOOK_PRIORITIES } from "../util/modding";
import { createPositionableButton, WCE_NAMESPACE } from "../util/publicApi";
import { fbcSettings } from "../util/settings";
import { deepCopy, parseJSON, isCharacter, isNonNullObject, drawTextFitLeft, fbcChatNotify } from "../util/utils";

Expand Down Expand Up @@ -219,6 +220,47 @@
return isNonNullObject(n) && typeof n.note === "string";
}

/**
* Reads the saved note for a member number. Part of the public `WCE.PastProfiles.Notes` API.
* @param {number} memberNumber
* @returns {Promise<FBCNote | undefined>}
*/
async function getNote(memberNumber) {
if (typeof memberNumber !== "number" || !Number.isFinite(memberNumber)) {
throw new TypeError("WCE.PastProfiles.Notes.get: memberNumber must be a finite number");
}
const note = await db.get("notes", memberNumber);
return isNote(note) ? note : undefined;

Check warning on line 233 in src/functions/pastProfiles.js

View workflow job for this annotation

GitHub Actions / lint

eslint(no-undefined)

src/functions/pastProfiles.js:233:34: Unexpected use of `undefined`
}

/**
* Writes (or overwrites) the saved note for a member number. Part of the public
* `WCE.PastProfiles.Notes` API. Keeps the note editor in sync if it's currently open for
* that same member.
* @param {number} memberNumber
* @param {string} note
* @returns {Promise<void>}
*/
async function setNote(memberNumber, note) {
if (typeof memberNumber !== "number" || !Number.isFinite(memberNumber)) {
throw new TypeError("WCE.PastProfiles.Notes.set: memberNumber must be a finite number");
}
if (typeof note !== "string") {
throw new TypeError("WCE.PastProfiles.Notes.set: note must be a string");
}
await quotaSafetyCheck();
const updatedAt = Date.now();
await db.put("notes", { memberNumber, note, updatedAt });
if (inNotes && InformationSheetSelection?.MemberNumber === memberNumber) {
noteInput.value = note;
noteUpdatedAt = updatedAt;
}
}

const { api: notesButtonApi, getPosition: getNotesButtonPosition, isHidden: isNotesButtonHidden } = createPositionableButton([1520, 60, 90, 90]);
WCE_NAMESPACE.Button = { ...WCE_NAMESPACE.Button, pastProfiles: notesButtonApi };
WCE_NAMESPACE.pastProfiles = { get: getNote, set: setNote };

function showNoteInput() {
if (!InformationSheetSelection?.MemberNumber) {
throw new Error("invalid InformationSheetSelection in notes");
Expand Down Expand Up @@ -284,7 +326,9 @@
DrawButton(1820, 60, 90, 90, "", "White", "Icons/Cancel.png", TextGet("LeaveNoSave"));
return null;
}
DrawButton(1520, 60, 90, 90, "", "White", "Icons/Notifications.png", displayText("[WCE] Notes"));
if (!isNotesButtonHidden()) {
DrawButton(...getNotesButtonPosition(), "", "White", "Icons/Notifications.png", displayText("[WCE] Notes"));
}
return next(args);
});

Expand All @@ -302,7 +346,7 @@
hideNoteInput();
}
return null;
} else if (!inNotes && MouseIn(1520, 60, 90, 90)) showNoteInput();
} else if (!inNotes && !isNotesButtonHidden() && MouseIn(...getNotesButtonPosition())) showNoteInput();
return next(args);
});

Expand Down
11 changes: 8 additions & 3 deletions src/functions/richOnlineProfile.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { displayText } from "../util/localization";
import { SDK, HOOK_PRIORITIES } from "../util/modding";
import { createPositionableButton, WCE_NAMESPACE } from "../util/publicApi";
import { fbcSettings } from "../util/settings";
import { processChatAugmentsForLine } from "./chatAugments";

Expand Down Expand Up @@ -74,12 +75,16 @@ export default function richOnlineProfile() {
return next(args);
});

const toggleEditButtonPos = /** @type {const} */ ([90, 60, 90, 90]);
const { api: editButtonApi, getPosition: getEditButtonPosition, isHidden: isEditButtonHidden } = createPositionableButton([90, 60, 90, 90]);
WCE_NAMESPACE.Button = { ...WCE_NAMESPACE.Button, EditProfile: editButtonApi };

SDK.hookFunction("OnlineProfileRun", HOOK_PRIORITIES.ModifyBehaviourMedium, (args, next) => {
if (!fbcSettings.richOnlineProfile) {
return next(args);
}
DrawButton(...toggleEditButtonPos, "", "White", "Icons/Crafting.png", displayText("Toggle Editing Mode"));
if (!isEditButtonHidden()) {
DrawButton(...getEditButtonPosition(), "", "White", "Icons/Crafting.png", displayText("Toggle Editing Mode"));
}

const ret = next(args);
if (!originalShown) {
Expand All @@ -93,7 +98,7 @@ export default function richOnlineProfile() {
if (!fbcSettings.richOnlineProfile) {
return next(args);
}
if (MouseIn(...toggleEditButtonPos)) {
if (!isEditButtonHidden() && MouseIn(...getEditButtonPosition())) {
if (originalShown) {
enableRichTextArea();
} else {
Expand Down
59 changes: 59 additions & 0 deletions src/util/publicApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Shared infrastructure for WCE's public JS API (`window.WCE.*`), used by other addons/userscripts
// to integrate with WCE without needing to hook its internals directly.

if (!globalThis.WCE) {
globalThis.WCE = {};
}

/** The single shared `WCE` namespace object. Feature modules attach their own sub-namespace to this. */
export const WCE_NAMESPACE: WCEPublicAPI = globalThis.WCE;

/**
* Creates a small stateful API that lets other addons reposition or temporarily hide a
* WCE-drawn screen button (e.g. because it overlaps with their own UI), without needing to know
* about the feature's internals. Returns the public API object plus internal accessors for the
* owning feature to use when drawing / hit-testing the button.
*/
export function createPositionableButton(defaultPosition: [number, number, number, number]): {

Check warning on line 17 in src/util/publicApi.ts

View workflow job for this annotation

GitHub Actions / lint

jsdoc(require-param)

src/util/publicApi.ts:17:42: Missing JSDoc `@param` declaration for function parameters.
api: WCEPositionableButtonAPI;
getPosition: () => [number, number, number, number];
isHidden: () => boolean;
isVisualHidden: () => boolean;
} {
let position: [number, number, number, number] = [...defaultPosition];
let hidden = false;
let visualHidden = false;

function setPosition(x: number, y: number, w: number, h: number): void {
for (const n of [x, y, w, h]) {
if (typeof n !== "number" || !Number.isFinite(n)) {
throw new TypeError("setPosition: x, y, w, h must all be finite numbers");
}
}
position = [x, y, w, h];
}

const api: WCEPositionableButtonAPI = {
getPosition: () => [...position],
setPosition,
resetPosition: () => {
position = [...defaultPosition];
},
hide: () => {
hidden = true;
},
show: () => {
hidden = false;
},
isHidden: () => hidden,
hideVisual: () => {
visualHidden = true;
},
showVisual: () => {
visualHidden = false;
},
isVisualHidden: () => visualHidden,
};

return { api, getPosition: () => position, isHidden: () => hidden, isVisualHidden: () => visualHidden };
}

Check warning on line 59 in src/util/publicApi.ts

View workflow job for this annotation

GitHub Actions / lint

jsdoc(require-returns)

src/util/publicApi.ts:17:8: Missing JSDoc `@returns` declaration for function.
64 changes: 64 additions & 0 deletions types/wce.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,70 @@ declare global {
var bcx: import("./bcxExternalInterface").BCX_ConsoleInterface | undefined;
var bcModSdk: import("bondage-club-mod-sdk").ModSDKGlobalAPI | undefined;
var FUSAM: FUSAMPublicAPI | undefined;

/**
* WCE's public JS API, exposed as `window.WCE`, so other addons/userscripts can integrate with
* it without hooking its internals directly. Sub-namespaces are added by the feature that owns
* them (e.g. `WCE.Messenger`), so this type grows as more features expose an API.
*/
var WCE: WCEPublicAPI;
type WCEPublicAPI = {
/** WCE-drawn screen buttons that other addons can reposition or temporarily hide. */
Button?: {
/**
* The instant messenger toggle button. Default position/size (in game canvas coordinates)
* is `[x, y, w, h] = [70, 905, 60, 60]`.
*/
Messenger?: WCEPositionableButtonAPI & {
/** Whether WCE's instant messenger feature is enabled in the user's settings. */
isEnabled: () => boolean;
/** Returns the CSS z-index of the instant messenger window. */
getZIndex: () => number;
/** Changes the CSS z-index of the instant messenger window. */
setZIndex: (zIndex: number) => void;
/** Restores the instant messenger window's default z-index (`100`). */
resetZIndex: () => void;
};
/**
* The "Toggle Editing Mode" (rich BIO) button on the online profile screen. Default
* position/size (in game canvas coordinates) is `[x, y, w, h] = [90, 60, 90, 90]`.
*/
EditProfile?: WCEPositionableButtonAPI;
/**
* The "[WCE] Notes" toggle button on the online profile screen. Default position/size (in
* game canvas coordinates) is `[x, y, w, h] = [1520, 60, 90, 90]`.
*/
pastProfiles?: WCEPositionableButtonAPI;
};
/** Reads and writes the per-member personal notes saved by the past-profiles feature. */
pastProfiles?: {
/** Returns the saved note for a member number, or `undefined` if none exists. */
get: (memberNumber: number) => Promise<FBCNote | undefined>;
/** Saves (overwriting) the note for a member number. */
set: (memberNumber: number, note: string) => Promise<void>;
};
};
/** Generic API for a WCE-drawn screen button that other addons can move or hide. */
type WCEPositionableButtonAPI = {
/** Returns the current `[x, y, w, h]` of the button in game canvas coordinates. */
getPosition: () => [number, number, number, number];
/** Moves the button to a new `x, y, w, h` position in game canvas coordinates. */
setPosition: (x: number, y: number, w: number, h: number) => void;
/** Restores the button to its default position. */
resetPosition: () => void;
/** Hides the button (and disables its click area) until `show()` is called. */
hide: () => void;
/** Reveals the button again after a previous `hide()` call. */
show: () => void;
/** Whether the button is currently hidden via this API. */
isHidden: () => boolean;
/** Hides only the button drawing while preserving its click area. */
hideVisual: () => void;
/** Draws the button again after `hideVisual()` was called. */
showVisual: () => void;
/** Whether only the button drawing is currently hidden. */
isVisualHidden: () => boolean;
};
type FUSAMPublicAPI = {
present: true;
addons: Record<string, FUSAMAddonState>;
Expand Down
Loading