• Support
  • Conflict: Guest Posting Extension

Hi Flarum devs,

I'm helping someone troubleshoot a conflict between their Guest Posting extension and askvortsov/rich-text.

Problem:

RTE's extend(TextEditor.prototype, 'controlItems', ...) accesses app.session.user.preferences(), causing TypeError: Cannot read properties of null (reading 'preferences') for guests. See applyEditor.js:

extend(TextEditor.prototype, 'controlItems', function (items) {
  items.add('rte', <Button className={classList({ active: app.session.user.preferences().useRichTextEditor })} />); // Error Here
});

Attempt:

The Guest Posting extension tries high-priority override:

override(TextEditor.prototype, 'controlItems', function (original) {
    if (!app.session.user) return new ItemList();
    return original();
}, -100);

Issue:

The TypeError still occurs, even with the override. extend seems to still execute/evaluate for guests despite the override.

Question:

How can the Guest Posting extension reliably prevent RTE's preferences() access only for guests? It seems the extend is being run. Goal: Guests=Markdown, Logged-in=Preference. Any ideas?

Thanks!


Full Code;

import app from 'flarum/forum/app';
import { extend, override } from 'flarum/common/extend';
import IndexPage from 'flarum/forum/components/IndexPage';
import DiscussionPage from 'flarum/forum/components/DiscussionPage';
import Component from 'flarum/common/Component';
import Button from 'flarum/common/components/Button';
import listItems from 'flarum/common/helpers/listItems';
import SignUpModal from 'flarum/forum/components/SignUpModal';
import Badge from 'flarum/common/components/Badge';
import CommentPost from 'flarum/forum/components/CommentPost';
import DiscussionComposer from 'flarum/forum/components/DiscussionComposer';
import ReplyComposer from 'flarum/forum/components/ReplyComposer';
import DiscussionControls from 'flarum/forum/utils/DiscussionControls';
import ReplyPlaceholder from 'flarum/forum/components/ReplyPlaceholder';
import avatar from 'flarum/common/helpers/avatar';
import username from 'flarum/common/helpers/username';
import ComposerPostPreview from 'flarum/forum/components/ComposerPostPreview';
import TextEditor from 'flarum/common/components/TextEditor';
import ItemList from 'flarum/common/utils/ItemList';

class GuestPromptAlert extends Component {
  view() {
    let message = '';
    const hasPostCount = app.forum.attribute('guestPostCount');
    const hasVoteCount = app.forum.attribute('guestVoteCount');

    if (hasPostCount && hasVoteCount) {
      message = app.translator.trans('guest-posting.forum.alert.create-account-posts-and-votes', { postCount: app.forum.attribute('guestPostCount'), voteCount: app.forum.attribute('guestVoteCount') });
    } else if (hasVoteCount) {
      message = app.translator.trans('guest-posting.forum.alert.create-account-votes-only', { count: app.forum.attribute('guestVoteCount') });
    } else {
      message = app.translator.trans('guest-posting.forum.alert.create-account', { count: app.forum.attribute('guestPostCount') });
    }

    return m('.Alert.Alert--info',
      m('.container', [
        m('span.Alert-body', message),
        m('ul.Alert-controls', listItems([
          Button.component({ className: 'Button Button--link', onclick: () => { app.modal.show(SignUpModal); } }, app.translator.trans('guest-posting.forum.alert.signup')),
        ])),
      ])
    );
  }
}

function addGuestPrompt(original, ...args) {
  const originalOutput = original.apply(this, args);
  if (!app.forum.attribute('guestPostCount') && !app.forum.attribute('guestVoteCount')) return originalOutput;
  const alert = GuestPromptAlert.component();
  if (Array.isArray(originalOutput)) {
    originalOutput.unshift(alert);
    return originalOutput;
  }
  return m('div', [alert, originalOutput]);
}

export function filterPostMentions(tag) {
    return false; // Disables mentions
  }

app.initializers.add('guest-posting', () => {
  override(IndexPage.prototype, 'hero', addGuestPrompt);
  override(DiscussionPage.prototype, 'view', addGuestPrompt);

  override(DiscussionControls, 'replyAction', function (original, start, isFullScreen) {
    if (app.session.user || !this.canReply()) return original(start, isFullScreen);

    return new Promise((resolve) => {
      if (!app.composer.composingReplyTo(this) || isFullScreen) {
         app.composer.load(ReplyComposer, { user: app.session.user, discussion: this, });
         app.composer.show();
      }

      if (start && app.viewingDiscussion(this) && !app.composer.isFullScreen()) {
        app.current.get('stream').goToNumber('reply');
      }

      resolve(app.composer);
    });
  });

  override(IndexPage.prototype, 'newDiscussionAction', function (original) {
    if (app.session.user || !app.forum.attribute('canStartDiscussion')) return original();

    return new Promise((resolve) => {
      app.composer.load(DiscussionComposer, { user: app.session.user, });
      app.composer.show();
      resolve(app.composer);
    });
  });

  extend(CommentPost.prototype, 'headerItems', function (items) {
    const guestUsername = this.attrs.post.attribute('guest_username');

    if (guestUsername) {
      if (items.has('user')) items.remove('user');
      items.add('guest-user', m('.PostUser', [
        m('h3', [
          m('span.Avatar.PostUser-avatar', guestUsername.charAt(0).toUpperCase()),
          m('span.username', guestUsername),
        ]),
        m('ul.PostUser-badges.badges', m('li', Badge.component({ type: 'guest', icon: 'fas fa-user-secret', label: app.translator.trans('guest-posting.forum.badge.guest'), }))),
      ]), 100);
    }
  });

  override(ReplyPlaceholder.prototype, 'view', function (original, ...args) {
    if (app.composer.composingReplyTo(this.attrs.discussion)) {
      return m('article', { className: 'Post CommentPost editing' },
        m('header', { className: 'Post-header' },
          m('div', { className: 'PostUser' },
            m('h3', null,
              avatar(app.session.user, { className: 'PostUser-avatar' }),
              username(app.session.user)
            ),
             app.session.user ? m('ul', { className: 'PostUser-badges badges' }, listItems(app.session.user.badges().toArray())) : null
          )
        ),
        m(ComposerPostPreview, { className: 'Post-body', composer: app.composer, surround: this.anchorPreview.bind(this) })
      );
    }
    return original.apply(this, args);
  });


  extend(SignUpModal.prototype, 'oninit', function () {
    this.importGuestContent = !!app.forum.attribute('guestPostCount') || !!app.forum.attribute('guestVoteCount');
  });

  extend(SignUpModal.prototype, 'fields', function (items) {
    if (app.forum.attribute('guestPostCount') || app.forum.attribute('guestVoteCount')) {
      items.add('guest-posting', m('.Form-group',
        m('div',
          m('label.checkbox', [
            m('input', { type: 'checkbox', checked: this.importGuestContent, onchange: () => { this.importGuestContent = !this.importGuestContent; }, disabled: this.loading }),
            app.forum.attribute('guestVoteCount')
              ? app.translator.trans('guest-posting.forum.modal.import-votes', { postCount: app.forum.attribute('guestPostCount') || '0', voteCount: app.forum.attribute('guestVoteCount') })
              : app.translator.trans('guest-posting.forum.modal.import', { count: app.forum.attribute('guestPostCount') })
          ])
        )
      ), 5);
    }
  });

  extend(SignUpModal.prototype, 'submitData', function (data) {
    if (this.importGuestContent) {
      data.importGuestContent = this.importGuestContent;
    }
  });

  const richTextExtensionId = 'askvortsov-rich-text';

  if (flarum.extensions[richTextExtensionId]) {
    console.log(`Guest Posting: RTE active. Applying RTE overrides.`);

    override(TextEditor.prototype, 'controlItems', function (original, items) {
      if (!app.session.user) return new ItemList();
      return original.apply(this, arguments);
    });

    override(TextEditor.prototype, 'toolbarItems', function (original, items) {
      if (!app.session.user) return new ItemList();
      return original.apply(this, arguments);
    });

    override(TextEditor.prototype, 'buildEditorParams', function(original) {
      if (!app.session.user) return {};
      return original.apply(this, arguments);
    });

    override(TextEditor.prototype, 'buildEditor', function (original, dom) {
      if (!app.session.user) {
        try {
          return original.apply(this, arguments);
        } catch (e) {
          console.error("Guest Posting: buildEditor failed for guest. Falling back.", e);
          return null;
        }
      }
      return original.apply(this, arguments);
    });
  }
});

I would be very grateful for any clues, guidance on where to look, or any information that could push me in the right direction.

I’m trying to update an extension that @clarkwinkelmann wrote a while ago. My goal is to make only markdown codes visible in the composer and override other extensions' added buttons. (e.g., disabling the WYSIWYG editor from the rich text extension)

You've probably seen this request dozens of times before. It's a highly requested feature, and if I can't get this to work, I may have to stop using the composer entirely.

huseyinfiliz the rte extension seems to be not compatible. You could fork it or offer a patch to support that app.session.user can be null in its context

I think I mentioned somewhere in a guest posting discussion that the easier solution would be to bind a user object to app.session.user that acts as an anonymous unauthorized user.. but yeah.

    luceos Honestly, instead of the first solution, I’m trying to solve this with an override. This is because not just this extension, but most extensions that add buttons to the composer cause issues.

    I’m considering removing all buttons except the markdown buttons for guests. This works for other extensions, but it doesn’t work with the RTE extension.

    That’s why the first solution doesn’t work. Of course, forking the RTE extension might work for a single site, but I wanted to make this solution available for everyone.

    The second solution, on the other hand, caused even more problems. There were dozens of issues even in Flarum’s core code. I’m not considering trying this solution at all.

    The closest I’ve gotten to a solution, as I mentioned, is through an override. However, this leads to an extension priority issue. From what I understand, an extension that is loaded earlier runs first.

      huseyinfiliz From what I understand, an extension that is loaded earlier runs first

      True. But you can define optional dependencies. Check the docs about this.