Skip to content
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

Platform-independent frontend slots #940

Draft
wants to merge 55 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
55 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
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
162 changes: 162 additions & 0 deletions extension/data/frontends/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Defines a system of "slots" which modules can use to render interface
// elements within the page. Slot locations 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 newRedditObserver from './newreddit';
import oldRedditObserver from './oldreddit';
import shredditObserver from './shreddit';

// FIXME: document all of these
interface PlatformSlotDetailsSubreddit {
fullname?: string;
name: string;
}

export type PlatformSlotDetailsUser = {
deleted: true;
} | {
deleted: false;
fullname?: string;
name: string;
};

interface PlatformSlotDetailsSubmission {
fullname: string;
}

interface PlatformSlotDetailsComment {
fullname: string;
}

// Slot names and the type of associated contextual information
// FIXME: document
export interface PlatformSlotDetails {
submissionAuthor: {
user: PlatformSlotDetailsUser;
submission?: PlatformSlotDetailsSubmission;
subreddit: PlatformSlotDetailsSubreddit;
// distinguishType: null | 'moderator' | 'employee' | 'alumnus';
// stickied: boolean;
};
commentAuthor: {
user: PlatformSlotDetailsUser;
comment: PlatformSlotDetailsComment;
submission?: PlatformSlotDetailsSubmission;
subreddit: PlatformSlotDetailsSubreddit;
// distinguished: boolean;
// stickied: boolean;
};
modmailAuthor: {
user: PlatformSlotDetailsUser;
subreddit: PlatformSlotDetailsSubreddit;
thread: {fullname: string};
message: {fullname: string};
// authorIsModerator: boolean;
// repliedAsSubreddit: boolean;
};
userHovercard: {
user: PlatformSlotDetailsUser;
subreddit: PlatformSlotDetailsSubreddit;
contextFullname?: string;
};
}
export type PlatformSlotLocation = 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<Location extends keyof PlatformSlotDetails> = ComponentType<{
details: PlatformSlotDetails[Location];
location: Location;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

location is a global name why did i think it was a good idea to name a parameter that. call it slotName or something

}>;

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

// FIXME: document
export function renderInSlots<K extends keyof PlatformSlotDetails> (locations: K[], render: PlatformSlotContent<K>) {
if (!Array.isArray(locations)) {
locations = [];
}
for (const location of locations) {
if (!slotConsumers[location]) {
slotConsumers[location] = [];
}
slotConsumers[location]?.push(render);
}
}

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

// FIXME: document
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: <Location extends keyof PlatformSlotDetails>(
location: Location,
details: PlatformSlotDetails[Location],
) => 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 location
// 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>(location: K, details: PlatformSlotDetails[K]) =>
reactRenderer(
<div
className='tb-platform-slot'
style={{display: 'inline-flex'}} // FIXME: do this in CSS
data-location={location}
>
{/* TODO: Do we want to do anything more sophisticated here? */}
{slotConsumers[location]?.map((Component, i) => (
<Component
key={i}
details={details}
location={location}
/>
))}
</div>,
);

// Initialize the appropriate observer for the platform we've loaded into
let observers = {
[RedditPlatform.OLD]: oldRedditObserver,
[RedditPlatform.NEW]: newRedditObserver,
[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