Skip to content

Platform-independent frontend slots #940

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 66 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
1300326
make helper for subtree mutationobservers that handle mutations serially
eritbh May 14, 2024
755cb1f
just uh. just kinda shove it all in there i guess
eritbh May 14, 2024
3dc35da
erin react doesnt even have slots what does this comment mean
eritbh May 14, 2024
92d5e2f
add key to mapped slot components
eritbh May 14, 2024
69e2907
update log names for consistency
eritbh May 14, 2024
0c8db28
ensure we use a sufficiently different jsapi consumer name
eritbh May 14, 2024
147531f
don't include details we can't get easily for now
eritbh May 14, 2024
3dff633
oldreddit observer based on oldreddit module code
eritbh May 14, 2024
b017242
more context data model revisions
eritbh May 14, 2024
557a955
modmail observer pulled even harder from tblistener
eritbh May 14, 2024
e73d528
fix unrelated bug with admin usernames in modmail sidebars
eritbh May 14, 2024
67b3ca6
put modnotes in the user hovercard location too
eritbh May 14, 2024
e80a144
guess we just dont need a logger there
eritbh May 14, 2024
b233f35
i am dumb
eritbh May 14, 2024
d1e2331
Merge branch 'master' into feat/platform-observers
eritbh May 14, 2024
d4735ad
convert usernotes to slots
eritbh May 16, 2024
ac59a9b
Use display:contents for jQ renderers
eritbh May 16, 2024
8da95e7
don't initialize observers until after all registrations
eritbh May 16, 2024
fff814c
use contextFullname when available for modnotes
eritbh May 16, 2024
900a6ac
return null for no render instead of empty fragment
eritbh May 16, 2024
fc2c7ec
Handle onlyshowInhover behavior at the listener level
eritbh May 16, 2024
7ebf831
convert usernotes module
eritbh May 16, 2024
22c4ccf
i knew it was gonna be a circular dependency issue
eritbh May 16, 2024
d60d042
have toolbox-generated things manage their own slots
eritbh May 18, 2024
499fa92
clean up import list since this is my fault
eritbh Aug 5, 2024
aadb07d
convert mod button to slots
eritbh Aug 5, 2024
4bf2571
Bump react and @types/react (#936)
dependabot[bot] May 16, 2024
37c1d21
Bump framer-motion from 11.0.3 to 11.2.2 (#944)
dependabot[bot] May 16, 2024
f94b21d
Bump webextension-polyfill from 0.10.0 to 0.12.0 (#942)
dependabot[bot] May 16, 2024
5fc0ad1
Bump react-dom and @types/react-dom (#945)
dependabot[bot] May 16, 2024
6ba2850
Bump dprint from 0.45.0 to 0.46.2 (#950)
dependabot[bot] Jun 12, 2024
1652c52
Bump @rollup/plugin-commonjs from 25.0.7 to 26.0.1 (#948)
dependabot[bot] Jun 12, 2024
645892d
Bump braces from 3.0.2 to 3.0.3 (#951)
dependabot[bot] Jun 12, 2024
182cee6
Bump inquirer from 9.2.15 to 9.3.2 (#955)
dependabot[bot] Jul 3, 2024
396e71e
Bump dprint from 0.46.2 to 0.47.0 (#956)
dependabot[bot] Jul 3, 2024
c92cf47
Bump typescript from 5.4.3 to 5.5.3 (#957)
dependabot[bot] Jul 3, 2024
6ec88fb
Bump iter-ops from 3.1.1 to 3.2.0 (#958)
dependabot[bot] Jul 12, 2024
b9c6025
Bump framer-motion from 11.2.2 to 11.3.2 (#962)
dependabot[bot] Jul 12, 2024
2c5a268
Bump inquirer from 9.3.2 to 10.0.1 (#960)
dependabot[bot] Jul 12, 2024
86e1b3c
Fix macros in modmail (#963)
eritbh Jul 16, 2024
ba20f72
More release meta fixes (#965)
eritbh Jul 16, 2024
3aaa711
v7.0.0-beta.4
eritbh Jul 16, 2024
01712b0
firefox is so picky about its version strings
eritbh Jul 16, 2024
8b6b5df
v7.0.0-beta.5
eritbh Jul 16, 2024
cd54af4
please accept this string i am begging you
eritbh Jul 16, 2024
820294e
v7.0.0-beta.6
eritbh Jul 16, 2024
7f55c1e
Bump iter-ops from 3.2.0 to 3.4.0 (#973)
dependabot[bot] Aug 5, 2024
66274d1
Bump inquirer from 10.0.1 to 10.1.7 (#972)
dependabot[bot] Aug 5, 2024
2efd9c9
Bump eslint-plugin-react from 7.34.0 to 7.35.0 (#966)
dependabot[bot] Aug 5, 2024
2ab3bca
fail CI if NOMERGE/XXX comments are present (#974)
eritbh Aug 5, 2024
ccc865f
fix up imports, add notes for removed settings
eritbh Aug 5, 2024
951b942
convert history button to slots
eritbh Aug 5, 2024
b0660cf
kinda start converting nuke button badly
eritbh Aug 5, 2024
852d157
Merge branch 'master' into feat/platform-observers
eritbh Oct 17, 2024
43a2606
fix import file extension
eritbh Oct 17, 2024
ad36bf4
Merge branch 'master' into feat/platform-observers
eritbh Dec 14, 2024
65ae711
remove newreddit observer since newreddit ded
eritbh Dec 14, 2024
34c2a64
adding documentation for new stuff is NOMERGE now
eritbh Dec 14, 2024
bf22d20
v7.0.0-beta.9
eritbh Dec 14, 2024
2013b4c
Bump @rollup/plugin-node-resolve from 15.3.0 to 16.0.0 (#1030)
dependabot[bot] Dec 16, 2024
38bf609
Bump dprint from 0.47.0 to 0.48.0 (#1032)
dependabot[bot] Dec 30, 2024
b663b3b
Bump inquirer from 12.2.0 to 12.3.0 (#1033)
dependabot[bot] Dec 30, 2024
935ec91
Bump framer-motion from 11.14.1 to 11.15.0 (#1031)
dependabot[bot] Dec 30, 2024
cad8d48
Bump framer-motion from 11.15.0 to 12.0.1 (#1041)
dependabot[bot] Jan 23, 2025
7468ab8
documentation; naming consistency
eritbh Jan 30, 2025
5972102
Merge branch 'master' into feat/platform-observers
eritbh Feb 3, 2025
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
226 changes: 226 additions & 0 deletions extension/data/frontends/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
// Defines a system of "slots" which modules can use to render interface
// elements within the page. Slot types are standardized for consumers (e.g. a
// module says it wants to display a button next to comment author usernames)
// and their actual position in the DOM is controlled by platform-specific
// observers responding to changes in the page and dynamically creating React
// roots which this code then populates with the appropriate contents.

// TODO: this file probably needs to be explained a lot better im in
// functionality hyperfocus mode not documentation hyperfocus mode

import {type ComponentType} from 'react';

import {currentPlatform, RedditPlatform} from '../util/platform';
import {reactRenderer} from '../util/ui_interop';

import modmailObserver from './modmail';
import oldRedditObserver from './oldreddit';
import shredditObserver from './shreddit';

/** Basic information about a subreddit. */
interface PlatformSlotDetailsSubreddit {
/** The subreddit's fullname, beginning with ``. */
fullname?: string;
/** The name of the subreddit */
name: string;
}

/** Basic information about a user. */
export type PlatformSlotDetailsUser = {
/** If `true`, this is a deleted user. */
deleted: true;
} | {
/** If `true`, this is a deleted user. */
deleted: false;
/** The user's fullname, starting with ``. */
fullname?: string;
/** The user's username. */
name: string;
};

/** Basic information about a submission. */
interface PlatformSlotDetailsSubmission {
/** The submission's fullname, beginning with ``. */
fullname: string;
}

/** Basic information about a comment. */
interface PlatformSlotDetailsComment {
/** The comment's fullname, beginning with ``. */
fullname: string;
}

// Slot names and the type of associated contextual information

/** Contextual information provided to consumers of each type of slot. */
export interface PlatformSlotDetails {
/** Details for a submission author slot. */
submissionAuthor: {
/** The author of this submission */
user: PlatformSlotDetailsUser;
/** The submission */
submission?: PlatformSlotDetailsSubmission;
/** The subreddit where this submission was posted */
subreddit: PlatformSlotDetailsSubreddit;
// /** The type of distinguish on the submission, if any */
// distinguishType: null | 'moderator' | 'employee' | 'alumnus';
// /** The sticky slot populated by the submission, if any */
// stickied: false | 1 | 2;
};
commentAuthor: {
/** The author of the comment */
user: PlatformSlotDetailsUser;
/** The comment */
comment: PlatformSlotDetailsComment;
/** The parent submission the comment was left under */
submission?: PlatformSlotDetailsSubmission;
/** The subreddit where the comment was posted */
subreddit: PlatformSlotDetailsSubreddit;
// /** The type of distinguish on the comment, if any */
// distinguished: boolean;
// /** Whether the comment is stickied */
// stickied: boolean;
};
modmailAuthor: {
/** The author of the message */
user: PlatformSlotDetailsUser;
/** The subreddit that initially received this message's thread */
subreddit: PlatformSlotDetailsSubreddit;
/** The thread this message is in */
thread: {fullname: string};
/** The message */
message: {fullname: string};
// /** Whether the author is a moderator */
// authorIsModerator: boolean;
// /** Whether this message was sent "as the subreddit" (with username hidden) */
// repliedAsSubreddit: boolean;
};
userHovercard: {
/** The user */
user: PlatformSlotDetailsUser;
/**
* The subreddit of the content the hovercard was triggered from. For
* example if the hovercard was triggered from an author name on a
* submission, this would be the subreddit it was submitted to; if the
* hovercard is triggered on a user in a modmail thread, this would be
* the subreddit that received the thread.
*/
subreddit: PlatformSlotDetailsSubreddit;
/**
* The fullname of the submission, comment, modmail thread, etc. the
* hovercard was triggered from
*/
contextFullname?: string;
};
}
/**
* A slot type. Describes a location on the page where slot contents can be
* rendered (e.g. `submissionAuthor` is a slot type that's rendered next to the
* usernames of submission authors).
*/
export type PlatformSlotType = keyof PlatformSlotDetails;

// Consumer code (used by toolbox modules)

// A consumer of a particular slot location which gets appropriate context and
// returns React content to be rendered in the slot
export type PlatformSlotContent<SlotType extends keyof PlatformSlotDetails> = ComponentType<{
/**
* Contextual details about the content the slot is attached to. Different
* slot types provide different information in this object.
*/
details: PlatformSlotDetails[SlotType];
/** The type of slot the component is currently being populated into. */
slotType: SlotType;
}>;

// Map of slot locations to consumers of the slot
const slotConsumers: {
[K in keyof PlatformSlotDetails]?: PlatformSlotContent<K>[];
} = Object.create(null);

/**
* Provide a consumer for one or more slot types. Whenever any of the `slots`
* appears on the page, the given component/renderer will be used to populate
* the slot (alongside any other consumers of the same slot type).
* @param slots An array of slots where the given component should be rendered
* @param render A React function component/render function that will be
* rendered in those slots. Props are passed which inform the component which
* type of slot it's currently being rendered in, and contextual information
* about the slot's surroundings.
*/
export function renderInSlots<K extends keyof PlatformSlotDetails> (slots: K[], render: PlatformSlotContent<K>) {
if (!Array.isArray(slots)) {
slots = [];
}
for (const slot of slots) {
if (!slotConsumers[slot]) {
slotConsumers[slot] = [];
}
slotConsumers[slot]?.push(render);
}
}

// Observer code (used by platform-specific observers in this directory)

/**
* A platform observer is a function responsible creating slot instances and
* attaching them to the page in the appropriate locations for a specific
* platform. It is called once when Toolbox starts and receives a function which
* creates slot instances, which it then inserts into the DOM.
*/
export type PlatformObserver = (
/**
* Creates a React root for a slot which will be populated with the
* appropriate contents. Observers are responsible for calling this function
* and inserting the resulting element into the DOM wherever the slot should
* be rendered.
*/
createRenderer: <SlotType extends keyof PlatformSlotDetails>(
slotType: SlotType,
details: PlatformSlotDetails[SlotType],
) => HTMLElement,
) => void;

// the actual `createRenderer` function observers get - returns a new react root
// which will contain all the contents different modules have registered for the
// given slot type
// NOTE: Exported because tbui builders need to manually emit their own slots.
// Should we just import this from the platform-specific bits instead of
// passing this function in to them?
export const createRenderer = <K extends keyof PlatformSlotDetails>(slotType: K, details: PlatformSlotDetails[K]) =>
reactRenderer(
<div
className='tb-platform-slot'
style={{display: 'inline-flex'}} // FIXME: do this in CSS
data-slot-type={slotType}
>
{/* TODO: Do we want to do anything more sophisticated here? */}
{slotConsumers[slotType]?.map((Component, i) => (
<Component
key={i}
details={details}
slotType={slotType}
/>
))}
</div>,
);

// Initialize the appropriate observer for the platform we've loaded into
let observers = {
[RedditPlatform.OLD]: oldRedditObserver,
[RedditPlatform.SHREDDIT]: shredditObserver,
[RedditPlatform.MODMAIL]: modmailObserver,
};

/**
* Start the platform observer, which will cause slots to be identified and
* populated. To be called as part of the init process after all slot consumers
* have been registered via {@linkcode renderInSlots}.
*/
export function initializeObserver () {
if (currentPlatform == null) {
return;
}
observers[currentPlatform](createRenderer);
}
96 changes: 96 additions & 0 deletions extension/data/frontends/modmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {getThingInfo} from '../tbcore';
import {PlatformObserver} from '.';

const MESSAGE_SEEN_CLASS = 'tb-observer-modmail-message-seen';

const SIDEBAR_SEEN_CLASS = 'tb-observer-modmail-sidebar-seen';

export default (createRenderer => {
function newModmailConversationAuthors () {
const $body = $('body');
const subreddit = $body.find('.ThreadTitle__community').text();
$body.find(`.Thread__message:not(.${MESSAGE_SEEN_CLASS})`).each(function () {
const $this = $(this);
this.classList.add(MESSAGE_SEEN_CLASS);

// Get information
const authorHref = $this.find('.Message__header .Message__author').attr('href');
const idDetails = $this.find('.m-link').attr('href')!.match(/\/mail\/.*?\/(.*?)\/(.*?)$/i)!;

this.querySelector('.Message__divider')?.after(createRenderer('modmailAuthor', {
user: authorHref === undefined
? {deleted: true}
: {deleted: false, name: authorHref.replace(/.*\/user\/([^/]+).*/, '$1')},
subreddit: {
name: subreddit,
},
thread: {
fullname: idDetails[1],
},
message: {
fullname: idDetails[2],
},
}));
});
}

/**
* Makes sure to fire a jsAPI `TBuserHovercard` event for new modmail sidebar instances.
* @function
*/
function newModmailSidebar () {
const $body = $('body');
if ($body.find('.ThreadViewer').length) {
const $modmailSidebar = $body.find(
`:is(.ThreadViewer__infobar, .ThreadViewerHeader__infobar, .InfoBar__idCard):not(.${SIDEBAR_SEEN_CLASS})`,
);
const jsApiPlaceHolder = `
<div class="tb-jsapi-container tb-modmail-sidebar-container">
<div class="InfoBar__recentsTitle">Toolbox functions:</div>
<span data-name="toolbox"></span>
</div>
`;
$modmailSidebar.each(function () {
getThingInfo(this, true).then(info => {
this.classList.add(SIDEBAR_SEEN_CLASS);

const $jsApiThingPlaceholder = $(jsApiPlaceHolder).appendTo(this);
const jsApiThingPlaceholder = $jsApiThingPlaceholder[0];

jsApiThingPlaceholder.appendChild(createRenderer('userHovercard', {
user: (info.user && info.user !== '[deleted]')
? {deleted: false, name: info.user}
: {deleted: true},
subreddit: {
name: info.subreddit,
},
}));
});
});
}
}

const $body = $('body');

$body.on('click', '.icon-user', () => {
setTimeout(() => {
newModmailSidebar();
}, 500);
});

$body.on('click', '.Thread__grouped', () => {
setTimeout(() => {
newModmailConversationAuthors();
}, 500);
});

window.addEventListener('TBNewPage', event => {
// TODO: augh
if ((event as any).detail.pageType === 'modmailConversation') {
setTimeout(() => {
newModmailSidebar();
newModmailConversationAuthors();
}, 500);
}
});
}) satisfies PlatformObserver;
Loading
Loading