Skip to content
View in the app

A better way to browse. Learn more.

ernestdefoe.online

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.
ernestdefoe.online

Extensions, themes & support for Flarum and Invision Community

Vibe coding for the community web. Report a bug, request a feature, or dig into the source — this is where the tools you use get built, in the open.

We do custom Bespoke Invision Community apps. If you have an idea for something you want then use the contact form to get in touch with us.
Knowledge base

Things that cost me a day, so they cost you none

Working notes from building Invision Community and Flarum applications. Mostly the failures that give no error at all — the ones where everything installs cleanly and quietly does the wrong thing.

92 articles

Invision Community 5

86 articles

Extensions and contracts

39

What each extension point is for, what it must declare, and what happens when it is wrong — which is usually nothing visible.

Languages and text

5

The string table, translation, and the places where text does not appear where you expected it to.

Theming, templates and forms

9

Theme hooks, CSS that survives both colour schemes, and building forms that do not throw on render.

Background work and scheduled tasks

5

The queue system, work that has to happen after the response, and jobs that finish without doing anything.

Data, settings and storage

11

The database layer, settings, tags, file storage, and backing up a live site.

AI features and expectations

5

What these features do, what they cost, and what buyers reasonably but wrongly assume they do.

Application structure and releases

11

The JSON files an application is made of, versioning and upgrade steps, and testing from the command line.

Realtime, chat and calls

1

WebSocket gateways, relays and the server-side pieces live features depend on — where "it works when I test it" and "it works for your members" are different claims.

Nothing matches that.

There is no event when a tag is created, and what that means for your app

Invision Community 5 fires 58 event-listener hooks. None of them concern tags.

Tags do not go through a model with a save() you can observe. They arrive through Content\Taggable::setTags(), which deletes the item's existing rows in core_tags and inserts the new ones directly. No event, no queue job, no notification. So there is no supported way to be told that a tag has come into existence.

The trap this sets

Any application whose state is derived from the tag list has to refresh that state somehow, and the easy place to do it is wherever the administrator already is:

if ( $values = $form->values() )
{
    $form->saveAsSettings( $values );
    Palette::refresh();          // rebuild derived state
    ...
}

That is correct as far as it goes, and it is also the entire failure. If the only callers of your refresh are ACP controllers, then your app is up to date exactly when an administrator is looking at it and at no other time. A tag created by a member, arriving in an import, or added by another application is never picked up.

Worse, this fails invisibly and in the most misleading way possible: the ACP screen is always right, because visiting it is what makes it right. The administrator sees correct state on the settings page and wrong state on the front end, and concludes the app is broken rather than stale.

A tag-colouring application shipped exactly this. Colours were only ever assigned, and its stylesheet only ever rebuilt, from the admin screen. Every tag created after setup kept the theme's default colour indefinitely.

Do not solve it in the request cycle

core/Loader::onFinish() runs on every front-end request and is tempting. Resist it for this: detecting a new tag means a grouped query over core_tags, and running that on every page view of the community is not a reasonable price for a cosmetic feature. (For work that genuinely must happen per request, see Running code on every request, after the page has been sent.)

Solve it with a task

A scheduled sweep is what core itself uses for comparable housekeeping.

// data/tasks.json  — the key must match the class and the filename
{"myappscan": "P0Y0M0DT0H15M0S"}
namespace IPS\myapp\tasks;

class myappscan extends \IPS\Task
{
    public function execute(): ?string
    {
        try
        {
            $done = Thing::refreshDerivedState();

            return $done ? "Updated {$done} things" : NULL;
        }
        catch ( \Throwable $e )
        {
            \IPS\Log::log( $e, 'myapp_scan' );

            /* Swallowed deliberately: core disables a task that keeps throwing,
               and losing the sweep permanently is worse than missing one run. */
            return NULL;
        }
    }

    public function cleanup(): void {}
}

Make the refresh free when nothing changed

🚨 A refresh written for a button press is usually written to assume something changed. Called every fifteen minutes, that becomes a settings write, and therefore a settings-cache invalidation across every node, four times an hour forever. Compare before writing:

public static function refresh(): string
{
    $css  = static::build();
    $hash = $css === '' ? '' : substr( md5( $css ), 0, 10 );

    if ( (string) Settings::i()->myapp_hash === $hash and (string) Settings::i()->myapp_css === $css )
    {
        return $hash;      // nothing to do, and now genuinely free
    }

    Settings::i()->changeValues( array( 'myapp_css' => $css, 'myapp_hash' => $hash ) );

    return $hash;
}

🚨 And the one that is easier to get wrong than any of the above

There are two tag tables and they mean different things:

core_tags_datathe tags defined for the community — what the ACP tag manager edits, and what Tag::getStore() returns (enabled only, recommended first, cached)
core_tagswhich items carry which tag — usage, not existence

Listing "the community's tags" by grouping core_tags looks right and is a real, plausible list. It is also wrong: a tag created in the tag manager and not yet applied to any content has no rows there, so it is invisible. The same application above shipped a screen for choosing tag colours that could not show a newly created tag at all — which is backwards, because creating a tag is exactly when you want to pick its colour, before anybody sees it in the default one.

Depending on what you are building you usually want the union, not either one:

/* defined — authoritative for spelling, since core_tags stores whatever
   case the item happened to be tagged with */
foreach ( \IPS\Content\Tag::getStore() as $tag )
{
    $key = mb_strtolower( (string) $tag );

    $text[ $key ] = (string) $tag;
    $uses[ $key ] = $uses[ $key ] ?? 0;
}

Keep the usage side too: if the community allows free-form tags, members create tags that exist in core_tags and nowhere else.

The general shape is worth internalising. Any query of the form "distinct values in the usage table" silently means "things that have already happened", which is not the same as "things that exist". It fails quietly, because what it returns is genuinely a list of real tags — just not the list you asked for.

Two things that bite while wiring this up

A task file that is not declared in data/tasks.json is never registered and never runs, with nothing anywhere saying so — the same silent-manifest family as a malformed listeners.json. Worth a static check in both directions: every declared task has a file, and every file is declared.

🚨 A settings field takes its language string from its own key. If a key of that exact name already exists for something else, adding the setting silently relabels the other thing. A setting named myapp_auto where myapp_auto is already a button label renames the button, with no error and no warning — you find it by noticing the button says something odd. Grep dev/lang.php for a key before naming a setting after it.

Tell the administrator what is going to happen

Once the sweep is periodic, state is briefly stale by design. Say so where they will look: "3 tags have no colour yet — these will be coloured automatically within about fifteen minutes, or press Colour them for me now." Showing pending state without saying what will resolve it reads exactly like the bug you just fixed.

Verified against

Invision Community 5.0.19, by reading system/Content/Taggable.php, system/Task/Task.php, and the complete set of hooks in system/Events/Event.php.



User Feedback

Recommended Comments

There are no comments to display.

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.