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.

The core/Notifications extension in Invision Community 5

The core/Notifications extension is what puts an entry behind the bell in the site header. It does two separate jobs for one type of member-facing notification. First, it declares the switches a member sees under Account Settings → Notification Settings — the rows that let somebody choose whether "somebody replied to content you follow" arrives in the notifications list, as a web push notification, as an email, or not at all. Second, it turns a stored notification row back into something readable: the title, the link, and the photo shown on /notifications/ and in the header dropdown. Without a registered extension, a notification can still be written to the database, but no member can ever configure it, so in practice it is never delivered, and if a row somehow exists nothing can render it (see the parse_ failure mode below).

This is the member-facing notification system. Administrator notifications — the bell in the AdminCP — are a different extension point, core/AdminNotifications, with a different base class and different tables.

Registration

The class lives at applications/<app>/extensions/core/Notifications/<Name>.php, is namespaced IPS\<app>\extensions\core\Notifications, extends IPS\Extensions\NotificationsAbstract, and must be listed in applications/<app>/data/extensions.json. Nothing scans the directory; the JSON is the only register. See the ContentRouter article for the details of that mechanism and its datastore cache — they are identical here.

Throughout the notification system the extension is identified by the key <appDirectory>_<ClassName>, because that is how Application::allExtensions() keys its return value (system/Application/Application.php line 419). An extension Files in the downloads app is downloads_Files. That key is used as the URL parameter for the settings page, and it is the language key that titles the group:

notifications__<appDirectory>_<ClassName>    e.g. notifications__downloads_Files

That string is looked up with Lang::get(), not addToStack(), in the AdminCP list at applications/core/modules/admin/membersettings/notifications.php line 78. That matters: Lang::get() with a single missing key throws UnderflowException (system/Lang/Lang.php line 861) and nothing in that controller catches it, so a missing group title takes out the whole AdminCP → Members → Notification Settings list rather than printing the raw key.

The contract

system/Extensions/NotificationsAbstract.php is short. One abstract method, three optional hooks, no properties:

abstract class NotificationsAbstract
{
    /* You MUST implement this. $member is the member whose settings page is
       being built, or NULL when the AdminCP is setting site-wide defaults. */
    abstract public static function configurationOptions( ?Member $member = NULL ): array;

    /* Called when a 'custom' or 'extra' control on the settings form is saved. */
    public static function saveExtra( ?Member $member, string $key, bool $value ) : void

    /* Called when a member switches off an entire delivery method. */
    public static function disableExtra( ?Member $member, string $method ) : void

    /* Called when an admin resets every member to the defaults. */
    public static function resetExtra() : void
}

Those four are the whole declared contract. Everything else this extension does is discovered by naming convention or by method_exists(), is not declared anywhere, and is therefore not type-checked.

configurationOptions() — the return shape

The method returns an array keyed by an arbitrary option key. Each entry has a type, and the type decides which keys are read. The form builder recognises four: standard, custom, separator and header (system/Notification/Notification.php lines 383–396). Only three of them are actually used anywhere in the shipped suite — grepping every core/Notifications extension turns up 24 standard, 3 custom and 2 separator entries and no header at all, so header is supported but untested by core.

A standard entry is the normal case — an inline/push/email row. From applications/core/extensions/core/Notifications/Profile.php lines 47–55:

$return['core_Profile_follow'] = array(
    'type'              => 'standard',
    'notificationTypes' => array( 'member_follow' ),
    'title'             => 'notifications__core_Profile_follow',
    'showTitle'         => TRUE,
    'description'       => 'notifications__core_Profile_follow_desc',
    'default'           => array( 'inline', 'push' ),
    'disabled'          => array(),
);
  • notificationTypes is the important one. It lists the notification keys — the string passed as the second argument to new IPS\Notification() — that this single row of switches controls. One row can govern several keys; applications/core/extensions/core/Notifications/Content.php line 111 puts new_content, new_comment and new_review behind one control.
  • default and disabled are arrays drawn from exactly three values: inline, push, email. These are stored in SET('email','inline','push') columns (applications/core/data/schema.json, core_notification_defaults), so no other value survives a write. disabled removes a method from the form entirely — Moderation's report centre row sets 'disabled' => array( 'inline', 'push' ) so the option is email-only.
  • title and description are language keys. showTitle is a boolean read by the row template as {{if \IPS\Dispatcher::i()->controllerLocation === 'admin' or $details['showTitle']}} — PHP short-circuits or, so it is only evaluated on the front end, and a missing key warns there but not in the AdminCP. Declare it anyway.
  • adminDescription is optional and replaces description on the AdminCP form only.
  • extra is an optional array of additional checkboxes that are not inline/push/email at all — each needs title, icon, value, and usually adminCanSetDefault. Saving one calls saveExtra().

A custom entry supplies a whole form field of your own under field, plus adminCanSetDefault and optionally admin_lang and adminOnly. A separator entry needs nothing but its type. A header entry needs a header key. Only standard entries create notification preferences; the others exist so that related settings can share the page.

The $member argument is how you hide options a member could never use. Return a smaller array, or an empty one — applications/core/extensions/core/Notifications/Achievements.php lines 50–65 returns [] outright when achievements and donation goals are both off, and Moderation.php line 60 gates its report-centre row on $member->canAccessReportCenter(). Remember that $member is NULL for the AdminCP, so every branch must cope with that.

The undeclared methods: parse_, parse_mobile_, parse_rest_

Nothing in the abstract mentions these, but without them a delivered notification has no content.

parse_<key>( Inline $notification, bool $htmlEscape = TRUE ): array renders the entry in the notifications list. It is an instance method. IPS\Notification\Inline::getData() (system/Notification/Inline.php lines 243–262) builds the method name from the stored key and calls it:

$method = "parse_{$this->notification_key}";

foreach ( $this->notification_app->extensions( 'core', 'Notifications' ) as $class )
{
    if ( method_exists( $class, $method ) )
    {
        $return = $class->$method( $this, $htmlEscape );

        if ( !isset( $return['unread'] ) )
        {
            $return['unread'] = !$this->read_time;
        }

        return $return;
    }
}
throw new RuntimeException;

Note $this->notification_app: only extensions belonging to the application that sent the notification are searched. The returned array uses title (required — the row template skips any notification without one), url, content, author and unread. Throwing OutOfRangeException from inside is the accepted way to say "the thing this was about has been deleted"; core does it in downloads/extensions/core/Notifications/Files.php line 78.

parse_mobile_<key>( Lang $language, ...$emailParams ): array renders the web push notification and must be public static. system/Notification/Notification.php lines 879–884 calls it statically and spreads the notification's email parameters as positional arguments:

$method = "parse_mobile_{$this->key}";
foreach ( $this->app->extensions( 'core', 'Notifications' ) as $class )
{
    if ( method_exists( $class, $method ) or $this->key === 'follower_content' )
    {
        $data = $class::$method( $language, ...$this->emailParams );

So the signature must match, in order and in type, the $emailParams array given to the Notification constructor. The returned array uses title, body (read without isset() in sendPushNotifications(), line 1009) and data, where data['url'] and data['author'] are recognised. Core's implementations also return a channelId; that key is not read anywhere in the self-hosted push path, and appears to be vestigial — unverified whether Cloud or the mobile app consumes it.

parse_rest_<key>( Inline $notification, bool $htmlEscape = TRUE ): array is optional and overrides parse_ for REST API responses only. system/Notification/RestApi.php line 32 looks for it and falls back to parent::getData() when it is absent. Only one implementation exists in the whole suite, MyStuff::parse_rest_new_likes().

Who calls it

The settings side. Most of these walk every extension in the suite with Application::allExtensions( 'core', 'Notifications' ); the two marked below instead use the per-application $app->extensions( 'core', 'Notifications' ):

  • IPS\Notification::defaultConfiguration(), system/Notification/Notification.php line 137 — walks every extension, reads every standard option, and inserts the missing rows into core_notification_defaults. This is where notification keys come into existence.
  • IPS\Notification::availableOptions() line 228, membersOptionCategories() line 307 and membersTypeForm() line 373 — build the member-facing form.
  • applications/core/modules/front/system/notifications.php lines 220 and 301 — Account Settings → Notification Settings, and the "stop sending me anything by email" action, which calls disableExtra() on every extension (line 328).
  • applications/core/modules/admin/membersettings/notifications.php lines 75, 186 and 395 — the AdminCP list, the per-extension defaults form (which calls saveExtra( NULL, ... ) at lines 305 and 367) and the "reset all members" action (which calls resetExtra()).
  • applications/core/extensions/core/MemberACPProfileBlocks/Notifications.php line 48 — the notification block on an AdminCP member profile.
  • IPS\Member::notificationsConfiguration(), system/Member/Member.php line 3143 — used to carry a member's saved preference across the other keys in the same notificationTypes group.
  • applications/core/api/GraphQL/Queries/NotificationTypes.php line 71, and Mutations/ChangeNotificationSetting.php line 87 — the latter is per-application: Application::load( $pieces[0] )->extensions( 'core', 'Notifications' ).
  • applications/core/modules/admin/developer/details.php lines 515, 724 and 1080 — the Developer Centre's translation and email-template checks, which call configurationOptions() with no arguments. Also per-application: $this->application->extensions( 'core', 'Notifications', false ).

The delivery side, scoped to one application via $app->extensions( 'core', 'Notifications' ): Notification::send() line 880 for push, and Inline::getData() / RestApi::getData() for display. Inline::getData() is in turn called from system/Notification/Table.php line 121, applications/core/modules/front/system/ajax.php line 578, applications/core/api/GraphQL/Types/MemberType.php line 557 and Mutations/MarkNotificationRead.php line 101.

Two more call sites use the extension but look for a method that no longer exists on the abstract — Application::installExtensions() line 2264 and the uninstall routine at line 5686, both guarded by method_exists( $class, 'getConfiguration' ). See the failure modes below.

Failure mode: the notification is sent and nobody receives anything

This is the one that costs the most time. Your code builds a Notification, attaches recipients, calls send(), and gets an empty result. No exception, no system log entry, no email, no row in core_notifications.

send() decides each delivery method by reading the member's resolved preferences:

if ( isset( $notificationPreferences[ $keyToCheck ] ) AND in_array( 'email', ... ) )

at system/Notification/Notification.php lines 704, 796 and 874. $notificationPreferences comes from Member::notificationsConfiguration(), which is a join of core_notification_defaults against core_notification_preferences. If your notification key has no row in core_notification_defaults, the isset() is false three times over and nothing at all happens.

For a conforming IPS 5 extension the rows are created in exactly one place, Notification::defaultConfiguration() — the two Db::i()->insert() calls at lines 190 and 201 inside the loop at 183–215. That method has only two callers in the whole suite: availableOptions() line 235, which is inside if ( $member ) and so only fires for a real member, and the AdminCP per-extension defaults form, membersettings/notifications.php line 193 in edit(). In other words the rows are created lazily, and send() never triggers it. (Two other places write to core_notification_defaultsApplication::installExtensions() line 2283 and the same AdminCP form's Db::i()->replace() at line 358 — but neither reaches a conforming extension before defaultConfiguration() has already run. See below.)

Application install does not help. Application::installExtensions() line 2264 seeds core_notification_defaults only for extensions that implement the legacy method getConfiguration(), which is not on the IPS 5 abstract and survives in exactly one core class, applications/core/extensions/core/Notifications/Moderation.php line 194. A conforming IPS 5 extension seeds nothing at install time.

The practical rule: after installing your app, open a member's Account Settings → Notification Settings once, or in the AdminCP go to Members → Notification Settings and click Edit on your extension's row, before testing delivery. Merely loading the AdminCP list is not enough — manage() only calls allExtensions() and Lang::get(), never defaultConfiguration(). If you want the rows to exist unconditionally, call IPS\Notification::defaultConfiguration() from your app's install or upgrade step. Two further consequences of the same design:

  • A notification key that appears in no extension's notificationTypes array can never be delivered by any method. There is no error for this; it is simply undeliverable forever.
  • Uninstalling your app does not remove your rows from core_notification_defaults or core_notification_preferences, because the uninstall routine at line 5686 uses the same getConfiguration() test. The orphaned rows are harmless but persist.

Failure mode: the notification key is longer than 32 characters

The two tables disagree. core_notification_defaults.notification_key and core_notification_preferences.notification_key are VARCHAR(100); core_notifications.notification_key — the table that holds the actual delivered notifications — is VARCHAR(32). All three are in applications/core/data/schema.json.

So a long key configures perfectly and then fails to store. On MySQL 8's default STRICT_TRANS_TABLES the insert raises a data-too-long error; with strict mode off the key is truncated, and the truncated string will not match parse_<key>, so the notification renders as nothing. Keep notification keys short. Core's longest is account_del_request_rejected at 28 characters (system/Member/PrivacyAction.php line 305) — four characters of headroom, which is how close the suite itself runs to the limit.

Failure mode: a parse_ method is missing

Inline::getData() throws a bare RuntimeException when no extension of the sending application declares parse_<key>. What the member sees depends entirely on which consumer hit it, and the consumers do not agree:

  • system/Notification/Table.php line 123 catches LogicException | RuntimeException and drops the row. The notification silently vanishes from /notifications/.
  • applications/core/modules/front/system/ajax.php line 580 catches only OutOfRangeException. A RuntimeException escapes, and the header's notification poll (do=instantNotifications) errors. That poll is attached in front/global/userBar.phtml and only runs when the auto_polling_enabled setting is on, so on a site with polling disabled this particular symptom does not appear.
  • applications/core/api/GraphQL/Types/MemberType.php line 559 catches only LogicException. RuntimeException is not one, so the GraphQL notifications query fails.

The tell for this class of bug is a permanently wrong unread count. Member::recountNotifications() (system/Member/Member.php line 4331) is a raw COUNT(*) over core_notifications with no attempt to parse anything, so an unrenderable notification still increments the badge. A bell showing "3" over an empty list means three rows exist that nothing can parse.

The same symptom appears if parse_<key> is in the right shape but in the wrong application. The loop iterates $this->notification_app->extensions(...) only. If you construct new Notification( Application::load('core'), 'my_key', ... ) from your own app, core is searched, your extension is not, and RuntimeException is thrown. Pass your own application.

Failure mode: a standard option is missing 'default' or 'disabled'

This one is loud, and it takes down a page that is not yours. defaultConfiguration() reads both keys with no guard and passes them to implode() (lines 201–205):

Db::i()->insert( 'core_notification_defaults', array(
    'notification_key' => $key,
    'default' => implode( ',', $data['default'] ),
    'disabled' => implode( ',', $data['disabled'] )
) );

A missing key gives an undefined-array-key warning and then implode(): Argument #2 ($array) must be of type array, null given — a TypeError. Because defaultConfiguration() is cached in a static property for the request only, this fires again for every member who opens notification settings and for the AdminCP per-extension defaults form, on a page owned by core, with your class in the trace. Always declare both, even as empty arrays. The same applies to notificationTypes, iterated without a guard at lines 157, 257 and 448.

Failure mode: a standard option is missing 'showTitle' or 'description'

Quieter. applications/core/dev/html/global/members/notificationsSettingsRow.phtml reads $details['showTitle'] and $details['description'] as direct array indexes, not through isset(). Missing keys produce PHP warnings on the settings page and, with display_errors on, visible noise inside the form. Both are cheap to declare; showTitle => FALSE is normal when the extension has only one option and the group title already says it.

Failure mode: an option with no 'type'

availableOptions() line 240 and membersTypeForm() lines 383–396 both branch on $details['type']. An entry with no type, or an unrecognised one, falls through every branch: it is copied into the returned settings array by availableOptions() (line 287) and then matches no if in the form builder. The option simply never appears. An unrecognised type is completely silent — no error, nothing in the logs, the row is just absent from the page. An entry with the type key omitted is not quite silent: $details['type'] is read as a direct index at defaultConfiguration() line 150, availableOptions() line 240, membersOptionCategories() line 332 and membersTypeForm() line 383, so each raises an undefined-array-key warning. On a production site with display_errors off you will still see nothing.

Failure mode: saveExtra() typed as the abstract declares it

The abstract says bool $value. That is correct for an extra checkbox, which is passed a real boolean at system/Notification/Notification.php line 468:

$extension::saveExtra( $member, $extraKey, array_key_exists( $extraKey, $values["notifications_{$key}"] ?? [] ) );

It is wrong for a custom option, which is passed the raw form value at line 461:

$extension::saveExtra( $member, $key, $values[ $key ] );

Core's own implementations widen the parameter to mixed for exactly this reason — Content.php line 155, Moderation.php line 168, Messenger.php line 86 — while BulkMails.php line 75, which only has an extra, keeps bool. PHP permits the widening because parameter types are contravariant.

If you keep bool and use a custom option, two things can happen, and the silent one is worse. A CheckboxSet hands you an array, which cannot be coerced, so you get a TypeError when a member saves the form. A Radio hands you a string; coercion is governed by the file the call is made from, and system/Notification/Notification.php does not declare(strict_types=1) — nothing in IPS's own code does, only some bundled libraries under system/3rd_party/ — so a non-empty string is coerced to TRUE and your setting is silently written as a boolean. Use mixed $value whenever you declare a custom option.

One more asymmetry in the same area: disableExtra() is called from the front end at applications/core/modules/front/system/notifications.php line 328 with Member::loggedIn(), never with NULL, but the signature says ?Member. Content::disableExtra() line 209 still tests $member !== null; BulkMails::disableExtra() line 105 does not and would fatal on a null. Test for null.

Failure mode: parse_mobile_ declared non-static, or with the wrong arguments

Because it is invoked as $class::$method( $language, ...$this->emailParams ), a non-static parse_mobile_ raises "Non-static method cannot be called statically", and a signature that does not match the $emailParams you passed to the Notification constructor raises ArgumentCountError or TypeError. Neither is caught. The fatal happens inside send(), which for most applications runs inside a background queue task, so the visible symptom is a stalled queue rather than an error on the page that triggered it — and only for members who have both enabled push and a registered PWA subscription, since line 874 gates the whole block on webPushEnabled() and count( $member->getPwaAuths() ). It is entirely possible to ship this bug and never see it in testing.

Failure mode: missing language strings and email templates

For a fully working notification type you need, at minimum:

  • notifications__<app>_<ClassName> — the group title. Fetched with Lang::get() in the AdminCP list, which throws UnderflowException on a missing key, so leaving this out breaks that page for everyone.
  • The title and description keys named by each option.
  • notification__<key> or whatever your parse_ method passes to addToStack(). Missing keys render as the key itself.
  • If email is not in disabled: an email template at applications/<app>/dev/email/notification_<key>.phtml (plus the matching .txt), because as soon as one recipient wants email, send() line 710 calls Email::buildFromTemplate( $this->app->directory, $this->emailKey, $this->emailParams, Email::TYPE_LIST ) with no check that the template exists, and emailKey defaults to 'notification_' . $key (line 557). Its <ips:template parameters=> must match your $emailParams, with $email appended.
  • The subject line mailsub__<app>_notification_<key> in dev/lang.php.

The Developer Centre will check most of this for you, but not the last item. applications/core/modules/admin/developer/details.php line 742 tests for the template with a path that is missing a directory separator:

if( file_exists( ROOT_PATH . "/applications/" . $this->application->directory . "dev/email/notification_" . $type . ".phtml" ) )

"downloads" . "dev/email/..." never exists, so the subject-line check inside that if never runs. A missing mailsub__ string is not reported and shows up as a raw language key in the subject of a live email. Check it by hand.

A minimal working example

applications/downloads/extensions/core/Notifications/Files.php is the smallest complete implementation in the suite — one option, one notification key, and both of the parse methods that matter (it has no parse_rest_; nothing outside MyStuff does). Reproduced below with the doc blocks removed and one line simplified: the real title is a ternary that uses notification__new_file_version_with instead when the category has version numbers turned on. Everything else is verbatim.

<?php

namespace IPS\downloads\extensions\core\Notifications;

use IPS\downloads\File;
use IPS\Extensions\NotificationsAbstract;
use IPS\Lang;
use IPS\Member;
use IPS\Notification\Inline;
use OutOfRangeException;
use function defined;

if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
    header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
    exit;
}

class Files extends NotificationsAbstract
{
    public static function configurationOptions( Member $member = NULL ): array
    {
        return array(
            'new_file_version'  => array(
                'type'              => 'standard',
                'notificationTypes' => array( 'new_file_version' ),
                'title'             => 'notifications__downloads_Files',
                'showTitle'         => FALSE,
                'description'       => 'notifications__downloads_Files_desc',
                'default'           => array( 'inline', 'push', 'email' ),
                'disabled'          => array()
            )
        );
    }

    public function parse_new_file_version( Inline $notification, bool $htmlEscape=TRUE ): array
    {
        $item = $notification->item;
        if ( !$item )
        {
            throw new OutOfRangeException;
        }

        return array(
            'title'     => Member::loggedIn()->language()->addToStack( 'notification__new_file_version', FALSE, array( 'sprintf' => array( $item->author()->name, $item->mapped('title') ) ) ),
            'url'       => $notification->item->url(),
            'content'   => $notification->item->content(),
            'author'    => $notification->extra ?: $notification->item->author(),
            'unread'    => (bool) ( $item->unread() )
        );
    }

    public static function parse_mobile_new_file_version( Lang $language, File $file ): array
    {
        return array(
            'title'     => $language->addToStack( 'notification__new_file_version_title' ),
            'body'      => $language->addToStack( 'notification__new_file_version', FALSE, array( 'htmlsprintf' => array(
                $file->author()->name,
                $file->mapped('title')
            ) ) ),
            'data'      => array(
                'url'       => (string) $file->url(),
                'author'    => $file->author()
            ),
            'channelId' => 'files',
        );
    }
}

One thing to change when you copy it: Files.php (and calendar/Events.php, and core/Achievements.php) declare configurationOptions( Member $member = NULL ), an implicit nullable. The abstract declares ?Member $member = NULL and implicit nullable parameter types are deprecated in PHP 8.4, so write the explicit ?Member in your own extension. Most core classes — Profile, Moderation, Content — already do.

The registration in applications/downloads/data/extensions.json:

{
    "core": {
        "Notifications": {
            "Files": "IPS\\downloads\\extensions\\core\\Notifications\\Files"
        }
    }
}

And the sending side, from applications/downloads/extensions/core/Queue/Notify.php line 78. Note that the second argument matches notificationTypes, and the fourth — array( $file ) — is what both the email template and parse_mobile_new_file_version() receive. Condensed — the real loop also checks $file->container()->can( 'view', $recipientMember ) before attaching, and records the notified rows:

$notification = new Notification( Application::load( 'downloads' ), 'new_file_version', $file, array( $file ) );

foreach( $recipients as $recipient )
{
    $notification->recipients->attach( Member::load( $recipient['notify_member_id'] ) );
}

$notification->send();

The supporting language strings in applications/downloads/dev/lang.php are notifications__downloads_Files, notifications__downloads_Files_desc, notification__new_file_version, notification__new_file_version_with, notification__new_file_version_title and mailsub__downloads_notification_new_file_version (lines 648–656 of that file), and the email template is applications/downloads/dev/email/notification_new_file_version.phtml.

What this extension is not for

Most applications never need one. Content-item notifications — replies, mentions, quotes, embeds, follows — are all raised against the core application: system/Content/Content.php lines 1473, 1492 and 1511 pass Application::load( 'core' ) for quote, mention and embed, and system/Content/Followable.php line 442 does the same for new_content. They are handled by applications/core/extensions/core/Notifications/Content.php and MyStuff.php. That is why forums, cms, nexus, gallery and blog ship no core/Notifications extension at all: their content is notified by core's. Only calendar (event reminders and RSVPs) and downloads (new file versions) declare one, because those events are not content posts.

Verified against

Invision Community 5.0.19 (applications/core/data/versions.json, highest entry 5001908 => "5.0.19"); PHP 8.1–8.3 per applications/core/data/requirements.json, with 8.1 listed as deprecated. Files read in full: system/Extensions/NotificationsAbstract.php; all nine implementations under applications/core/extensions/core/Notifications/; applications/downloads/extensions/core/Notifications/Files.php and applications/calendar/extensions/core/Notifications/Events.php. Also inspected: system/Notification/Notification.php (defaultConfiguration(), availableOptions(), membersOptionCategories(), membersTypeForm(), send(), sendPushNotifications()), system/Notification/Inline.php, system/Notification/RestApi.php, system/Notification/Table.php, system/Member/Member.php (notificationsConfiguration(), recountNotifications()), system/Application/Application.php (allExtensions(), extensions(), constructExtensionClass(), installExtensions(), delete()), applications/core/modules/front/system/notifications.php, applications/core/modules/front/system/ajax.php, applications/core/modules/admin/membersettings/notifications.php, applications/core/modules/admin/developer/details.php, applications/core/extensions/core/MemberACPProfileBlocks/Notifications.php, applications/core/api/GraphQL/Queries/NotificationTypes.php, applications/core/api/GraphQL/Types/MemberType.php, applications/core/api/GraphQL/Mutations/MarkNotificationRead.php, system/Lang/Lang.php (get(), addToStack()), system/Content/Content.php and system/Content/Followable.php, system/Member/PrivacyAction.php, the templates global/members/notificationsSettingsRow.phtml, front/system/notificationsRows.phtml and front/global/userBar.phtml, and applications/core/data/schema.json for core_notifications, core_notification_defaults and core_notification_preferences. A grep for channelId across the whole install finds it only in the extensions that produce it and in no consumer, which is the basis for calling it vestigial; whether Invision Community Cloud or the mobile application reads it is not verifiable from the self-hosted source. Likewise the $this->key === 'follower_content' branch at Notification.php line 882: grepping every new Notification( in the suite confirms no notification is ever constructed with that key (follower_content appears only as $keyToCheck, assigned at line 688), so the branch is dead in shipped code — but its intended behaviour is undocumented and untested here.


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.