mirror of
https://github.com/flarum/framework.git
synced 2024-11-29 12:43:52 +08:00
Update for new API + TextFormatter
This commit is contained in:
parent
985ccc096e
commit
da25882280
32
extensions/mentions/.editorconfig
Normal file
32
extensions/mentions/.editorconfig
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# EditorConfig helps developers define and maintain consistent
|
||||||
|
# coding styles between different editors and IDEs
|
||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.js]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.{css,less}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.html]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.{diff,md}]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.php]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
5
extensions/mentions/.eslintignore
Normal file
5
extensions/mentions/.eslintignore
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
**/bower_components/**/*
|
||||||
|
**/node_modules/**/*
|
||||||
|
vendor/**/*
|
||||||
|
**/Gulpfile.js
|
||||||
|
**/dist/**/*
|
171
extensions/mentions/.eslintrc
Normal file
171
extensions/mentions/.eslintrc
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
{
|
||||||
|
"parser": "babel-eslint", // https://github.com/babel/babel-eslint
|
||||||
|
"env": { // http://eslint.org/docs/user-guide/configuring.html#specifying-environments
|
||||||
|
"browser": true // browser global variables
|
||||||
|
},
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"arrowFunctions": true,
|
||||||
|
"blockBindings": true,
|
||||||
|
"classes": true,
|
||||||
|
"defaultParams": true,
|
||||||
|
"destructuring": true,
|
||||||
|
"forOf": true,
|
||||||
|
"generators": false,
|
||||||
|
"modules": true,
|
||||||
|
"objectLiteralComputedProperties": true,
|
||||||
|
"objectLiteralDuplicateProperties": false,
|
||||||
|
"objectLiteralShorthandMethods": true,
|
||||||
|
"objectLiteralShorthandProperties": true,
|
||||||
|
"spread": true,
|
||||||
|
"superInFunctions": true,
|
||||||
|
"templateStrings": true,
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"m": true,
|
||||||
|
"app": true,
|
||||||
|
"$": true,
|
||||||
|
"moment": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
/**
|
||||||
|
* Strict mode
|
||||||
|
*/
|
||||||
|
// babel inserts "use strict"; for us
|
||||||
|
"strict": [2, "never"], // http://eslint.org/docs/rules/strict
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ES6
|
||||||
|
*/
|
||||||
|
"no-var": 2, // http://eslint.org/docs/rules/no-var
|
||||||
|
"prefer-const": 2, // http://eslint.org/docs/rules/prefer-const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variables
|
||||||
|
*/
|
||||||
|
"no-shadow": 2, // http://eslint.org/docs/rules/no-shadow
|
||||||
|
"no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names
|
||||||
|
"no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars
|
||||||
|
"vars": "local",
|
||||||
|
"args": "after-used"
|
||||||
|
}],
|
||||||
|
"no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible errors
|
||||||
|
*/
|
||||||
|
"comma-dangle": [2, "never"], // http://eslint.org/docs/rules/comma-dangle
|
||||||
|
"no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign
|
||||||
|
"no-console": 1, // http://eslint.org/docs/rules/no-console
|
||||||
|
"no-debugger": 1, // http://eslint.org/docs/rules/no-debugger
|
||||||
|
"no-alert": 1, // http://eslint.org/docs/rules/no-alert
|
||||||
|
"no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition
|
||||||
|
"no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys
|
||||||
|
"no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case
|
||||||
|
"no-empty": 2, // http://eslint.org/docs/rules/no-empty
|
||||||
|
"no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign
|
||||||
|
"no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast
|
||||||
|
"no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi
|
||||||
|
"no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign
|
||||||
|
"no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations
|
||||||
|
"no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp
|
||||||
|
"no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace
|
||||||
|
"no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls
|
||||||
|
"no-reserved-keys": 2, // http://eslint.org/docs/rules/no-reserved-keys
|
||||||
|
"no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays
|
||||||
|
"no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable
|
||||||
|
"use-isnan": 2, // http://eslint.org/docs/rules/use-isnan
|
||||||
|
"block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best practices
|
||||||
|
*/
|
||||||
|
"consistent-return": 2, // http://eslint.org/docs/rules/consistent-return
|
||||||
|
"curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly
|
||||||
|
"default-case": 2, // http://eslint.org/docs/rules/default-case
|
||||||
|
"dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation
|
||||||
|
"allowKeywords": true
|
||||||
|
}],
|
||||||
|
"eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq
|
||||||
|
"no-caller": 2, // http://eslint.org/docs/rules/no-caller
|
||||||
|
"no-else-return": 2, // http://eslint.org/docs/rules/no-else-return
|
||||||
|
"no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null
|
||||||
|
"no-eval": 2, // http://eslint.org/docs/rules/no-eval
|
||||||
|
"no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native
|
||||||
|
"no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind
|
||||||
|
"no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough
|
||||||
|
"no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal
|
||||||
|
"no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval
|
||||||
|
"no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks
|
||||||
|
"no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func
|
||||||
|
"no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str
|
||||||
|
"no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign
|
||||||
|
"no-new": 2, // http://eslint.org/docs/rules/no-new
|
||||||
|
"no-new-func": 2, // http://eslint.org/docs/rules/no-new-func
|
||||||
|
"no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers
|
||||||
|
"no-octal": 2, // http://eslint.org/docs/rules/no-octal
|
||||||
|
"no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape
|
||||||
|
"no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign
|
||||||
|
"no-proto": 2, // http://eslint.org/docs/rules/no-proto
|
||||||
|
"no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare
|
||||||
|
"no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign
|
||||||
|
"no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare
|
||||||
|
"no-sequences": 2, // http://eslint.org/docs/rules/no-sequences
|
||||||
|
"no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal
|
||||||
|
"no-with": 2, // http://eslint.org/docs/rules/no-with
|
||||||
|
"radix": 2, // http://eslint.org/docs/rules/radix
|
||||||
|
"vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top
|
||||||
|
"wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife
|
||||||
|
"yoda": 2, // http://eslint.org/docs/rules/yoda
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Style
|
||||||
|
*/
|
||||||
|
"indent": [2, 2], // http://eslint.org/docs/rules/indent
|
||||||
|
"brace-style": [2, // http://eslint.org/docs/rules/brace-style
|
||||||
|
"1tbs", {
|
||||||
|
"allowSingleLine": true
|
||||||
|
}],
|
||||||
|
"quotes": [
|
||||||
|
2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes
|
||||||
|
],
|
||||||
|
"camelcase": [2, { // http://eslint.org/docs/rules/camelcase
|
||||||
|
"properties": "never"
|
||||||
|
}],
|
||||||
|
"comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing
|
||||||
|
"before": false,
|
||||||
|
"after": true
|
||||||
|
}],
|
||||||
|
"comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style
|
||||||
|
"eol-last": 2, // http://eslint.org/docs/rules/eol-last
|
||||||
|
"func-names": 1, // http://eslint.org/docs/rules/func-names
|
||||||
|
"key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing
|
||||||
|
"beforeColon": false,
|
||||||
|
"afterColon": true
|
||||||
|
}],
|
||||||
|
"new-cap": [2, { // http://eslint.org/docs/rules/new-cap
|
||||||
|
"newIsCap": true
|
||||||
|
}],
|
||||||
|
"no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines
|
||||||
|
"max": 2
|
||||||
|
}],
|
||||||
|
"no-new-object": 2, // http://eslint.org/docs/rules/no-new-object
|
||||||
|
"no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func
|
||||||
|
"no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces
|
||||||
|
"no-wrap-func": 2, // http://eslint.org/docs/rules/no-wrap-func
|
||||||
|
"no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle
|
||||||
|
"one-var": [2, "never"], // http://eslint.org/docs/rules/one-var
|
||||||
|
"padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks
|
||||||
|
"semi": [2, "always"], // http://eslint.org/docs/rules/semi
|
||||||
|
"semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing
|
||||||
|
"before": false,
|
||||||
|
"after": true
|
||||||
|
}],
|
||||||
|
"space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords
|
||||||
|
"space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks
|
||||||
|
"space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren
|
||||||
|
"space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops
|
||||||
|
"space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case
|
||||||
|
"spaced-line-comment": 2, // http://eslint.org/docs/rules/spaced-line-comment
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
// Require the extension's composer autoload file. This will enable all of our
|
|
||||||
// classes in the src directory to be autoloaded.
|
|
||||||
require __DIR__.'/vendor/autoload.php';
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
// Register our service provider with the Flarum application. In here we can
|
return 'Flarum\Mentions\Extension';
|
||||||
// register bindings and execute code when the application boots.
|
|
||||||
return $this->app->register('Flarum\Mentions\MentionsServiceProvider');
|
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "flarum-mentions",
|
"name": "mentions",
|
||||||
"title": "Mentions",
|
"title": "Mentions",
|
||||||
"description": "Mention and reply to specific posts and users.",
|
"description": "Mention and reply to specific posts and users.",
|
||||||
"tags": [
|
"keywords": ["discussions"],
|
||||||
"discussions"
|
|
||||||
],
|
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Toby Zerner",
|
"name": "Toby Zerner",
|
||||||
|
@ -16,8 +14,8 @@
|
||||||
"php": ">=5.4.0",
|
"php": ">=5.4.0",
|
||||||
"flarum": ">0.1.0"
|
"flarum": ">0.1.0"
|
||||||
},
|
},
|
||||||
"links": {
|
"support": {
|
||||||
"github": "https://github.com/flarum/mentions",
|
"source": "https://github.com/flarum/mentions",
|
||||||
"issues": "https://github.com/flarum/mentions/issues"
|
"issues": "https://github.com/flarum/mentions/issues"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
44
extensions/mentions/js/bootstrap.js
vendored
44
extensions/mentions/js/bootstrap.js
vendored
|
@ -1,44 +0,0 @@
|
||||||
import app from 'flarum/app';
|
|
||||||
import SettingsPage from 'flarum/components/settings-page';
|
|
||||||
import { extend } from 'flarum/extension-utils';
|
|
||||||
import icon from 'flarum/helpers/icon';
|
|
||||||
|
|
||||||
import postMentionPreviews from 'flarum-mentions/post-mention-previews';
|
|
||||||
import mentionedByList from 'flarum-mentions/mentioned-by-list';
|
|
||||||
import postReplyAction from 'flarum-mentions/post-reply-action';
|
|
||||||
import composerAutocomplete from 'flarum-mentions/composer-autocomplete';
|
|
||||||
import PostMentionedNotification from 'flarum-mentions/components/post-mentioned-notification';
|
|
||||||
import UserMentionedNotification from 'flarum-mentions/components/user-mentioned-notification';
|
|
||||||
|
|
||||||
app.initializers.add('flarum-mentions', function() {
|
|
||||||
// For every mention of a post inside a post's content, set up a hover handler
|
|
||||||
// that shows a preview of the mentioned post.
|
|
||||||
postMentionPreviews();
|
|
||||||
|
|
||||||
// In the footer of each post, show information about who has replied (i.e.
|
|
||||||
// who the post has been mentioned by).
|
|
||||||
mentionedByList();
|
|
||||||
|
|
||||||
// Add a 'reply' control to the footer of each post. When clicked, it will
|
|
||||||
// open up the composer and add a post mention to its contents.
|
|
||||||
postReplyAction();
|
|
||||||
|
|
||||||
// After typing '@' in the composer, show a dropdown suggesting a bunch of
|
|
||||||
// posts or users that the user could mention.
|
|
||||||
composerAutocomplete();
|
|
||||||
|
|
||||||
app.notificationComponentRegistry['postMentioned'] = PostMentionedNotification;
|
|
||||||
app.notificationComponentRegistry['userMentioned'] = UserMentionedNotification;
|
|
||||||
|
|
||||||
// Add notification preferences.
|
|
||||||
extend(SettingsPage.prototype, 'notificationTypes', function(items) {
|
|
||||||
items.add('postMentioned', {
|
|
||||||
name: 'postMentioned',
|
|
||||||
label: [icon('reply'), ' Someone replies to my post']
|
|
||||||
});
|
|
||||||
items.add('userMentioned', {
|
|
||||||
name: 'userMentioned',
|
|
||||||
label: [icon('at'), ' Someone mentions me in a post']
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,7 +1,7 @@
|
||||||
var gulp = require('flarum-gulp');
|
var gulp = require('flarum-gulp');
|
||||||
|
|
||||||
gulp({
|
gulp({
|
||||||
modulePrefix: 'flarum-mentions',
|
modulePrefix: 'mentions',
|
||||||
files: [
|
files: [
|
||||||
'bower_components/textarea-caret-position/index.js'
|
'bower_components/textarea-caret-position/index.js'
|
||||||
]
|
]
|
|
@ -1,34 +1,34 @@
|
||||||
import { extend } from 'flarum/extension-utils';
|
/*global getCaretCoordinates*/
|
||||||
import ComposerBody from 'flarum/components/composer-body';
|
|
||||||
import ReplyComposer from 'flarum/components/reply-composer';
|
import { extend } from 'flarum/extend';
|
||||||
import EditComposer from 'flarum/components/edit-composer';
|
import ComposerBody from 'flarum/components/ComposerBody';
|
||||||
import avatar from 'flarum/helpers/avatar';
|
import avatar from 'flarum/helpers/avatar';
|
||||||
import username from 'flarum/helpers/username';
|
import usernameHelper from 'flarum/helpers/username';
|
||||||
import highlight from 'flarum/helpers/highlight';
|
import highlight from 'flarum/helpers/highlight';
|
||||||
import truncate from 'flarum/utils/truncate';
|
import { truncate } from 'flarum/utils/string';
|
||||||
|
|
||||||
import AutocompleteDropdown from 'flarum-mentions/components/autocomplete-dropdown';
|
import AutocompleteDropdown from 'mentions/components/AutocompleteDropdown';
|
||||||
|
|
||||||
export default function() {
|
export default function addComposerAutocomplete() {
|
||||||
extend(ComposerBody.prototype, 'config', function(original, element, isInitialized, context) {
|
extend(ComposerBody.prototype, 'config', function(original, isInitialized) {
|
||||||
if (isInitialized) return;
|
if (isInitialized) return;
|
||||||
|
|
||||||
var composer = this;
|
const composer = this;
|
||||||
var $container = $('<div class="mentions-dropdown-container"></div>');
|
const $container = $('<div class="ComposerBody-mentionsDropdownContainer"></div>');
|
||||||
var dropdown = new AutocompleteDropdown({items: []});
|
const dropdown = new AutocompleteDropdown({items: []});
|
||||||
var typed;
|
const $textarea = this.$('textarea');
|
||||||
var mentionStart;
|
const searched = [];
|
||||||
var $textarea = this.$('textarea');
|
let mentionStart;
|
||||||
var searched = [];
|
let typed;
|
||||||
var searchTimeout;
|
let searchTimeout;
|
||||||
|
|
||||||
var applySuggestion = function(replacement) {
|
const applySuggestion = function(replacement) {
|
||||||
replacement += ' ';
|
const insert = replacement + ' ';
|
||||||
|
|
||||||
var content = composer.content();
|
const content = composer.content();
|
||||||
composer.editor.setContent(content.substring(0, mentionStart - 1)+replacement+content.substr($textarea[0].selectionStart));
|
composer.editor.setValue(content.substring(0, mentionStart - 1) + insert + content.substr($textarea[0].selectionStart));
|
||||||
|
|
||||||
var index = mentionStart - 1 + replacement.length;
|
const index = mentionStart - 1 + insert.length;
|
||||||
composer.editor.setSelectionRange(index, index);
|
composer.editor.setSelectionRange(index, index);
|
||||||
|
|
||||||
dropdown.hide();
|
dropdown.hide();
|
||||||
|
@ -41,73 +41,76 @@ export default function() {
|
||||||
// Up, down, enter, tab, escape, left, right.
|
// Up, down, enter, tab, escape, left, right.
|
||||||
if ([9, 13, 27, 40, 38, 37, 39].indexOf(e.which) !== -1) return;
|
if ([9, 13, 27, 40, 38, 37, 39].indexOf(e.which) !== -1) return;
|
||||||
|
|
||||||
var cursor = this.selectionStart;
|
const cursor = this.selectionStart;
|
||||||
|
|
||||||
if (this.selectionEnd - cursor > 0) return;
|
if (this.selectionEnd - cursor > 0) return;
|
||||||
|
|
||||||
// Search backwards from the cursor for an '@' symbol, without any
|
// Search backwards from the cursor for an '@' symbol, without any
|
||||||
// intervening whitespace. If we find one, we will want to show the
|
// intervening whitespace. If we find one, we will want to show the
|
||||||
// autocomplete dropdown!
|
// autocomplete dropdown!
|
||||||
var value = this.value;
|
const value = this.value;
|
||||||
mentionStart = 0;
|
mentionStart = 0;
|
||||||
for (var i = cursor - 1; i >= 0; i--) {
|
for (let i = cursor - 1; i >= 0; i--) {
|
||||||
var character = value.substr(i, 1);
|
const character = value.substr(i, 1);
|
||||||
if (/\s/.test(character)) break;
|
if (/\s/.test(character)) break;
|
||||||
if (character == '@') {
|
if (character === '@') {
|
||||||
mentionStart = i + 1;
|
mentionStart = i + 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dropdown.hide();
|
dropdown.hide();
|
||||||
dropdown.active(false);
|
dropdown.active = false;
|
||||||
|
|
||||||
if (mentionStart) {
|
if (mentionStart) {
|
||||||
typed = value.substring(mentionStart, cursor).toLowerCase();
|
typed = value.substring(mentionStart, cursor).toLowerCase();
|
||||||
|
|
||||||
var makeSuggestion = function(user, replacement, content, className) {
|
const makeSuggestion = function(user, replacement, content, className = '') {
|
||||||
return m('a[href=javascript:;].post-preview', {
|
const username = usernameHelper(user);
|
||||||
className,
|
if (typed) {
|
||||||
onclick: () => applySuggestion(replacement),
|
username.children[0] = highlight(username.children[0], typed);
|
||||||
onmouseenter: function() { dropdown.setIndex($(this).parent().index()); }
|
}
|
||||||
}, m('div.post-preview-content', [
|
|
||||||
avatar(user),
|
return (
|
||||||
(function() {
|
<button className={'PostPreview ' + className}
|
||||||
var vdom = username(user);
|
onclick={() => applySuggestion(replacement)}
|
||||||
if (typed) {
|
onmouseenter={function() {
|
||||||
vdom.children[0] = highlight(vdom.children[0], typed);
|
dropdown.setIndex($(this).parent().index());
|
||||||
}
|
}}>
|
||||||
return vdom;
|
<span className="PostPreview-content">
|
||||||
})(), ' ',
|
{avatar(user)}
|
||||||
content
|
{username}{' '}
|
||||||
]));
|
{content}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
var buildSuggestions = () => {
|
const buildSuggestions = () => {
|
||||||
var suggestions = [];
|
const suggestions = [];
|
||||||
|
|
||||||
// If the user is replying to a discussion, or if they are editing a
|
// If the user is replying to a discussion, or if they are editing a
|
||||||
// post, then we can suggest other posts in the discussion to mention.
|
// post, then we can suggest other posts in the discussion to mention.
|
||||||
// We will add the 5 most recent comments in the discussion which
|
// We will add the 5 most recent comments in the discussion which
|
||||||
// match any username characters that have been typed.
|
// match any username characters that have been typed.
|
||||||
var composerPost = composer.props.post;
|
const composerPost = composer.props.post;
|
||||||
var discussion = (composerPost && composerPost.discussion()) || composer.props.discussion;
|
const discussion = (composerPost && composerPost.discussion()) || composer.props.discussion;
|
||||||
if (discussion) {
|
if (discussion) {
|
||||||
discussion.posts()
|
discussion.posts()
|
||||||
.filter(post => post && post.contentType() === 'comment' && (!composerPost || post.number() < composerPost.number()))
|
.filter(post => post && post.contentType() === 'comment' && (!composerPost || post.number() < composerPost.number()))
|
||||||
.sort((a, b) => b.time() - a.time())
|
.sort((a, b) => b.time() - a.time())
|
||||||
.filter(post => {
|
.filter(post => {
|
||||||
var user = post.user();
|
const user = post.user();
|
||||||
return user && user.username().toLowerCase().substr(0, typed.length) === typed;
|
return user && user.username().toLowerCase().substr(0, typed.length) === typed;
|
||||||
})
|
})
|
||||||
.splice(0, 5)
|
.splice(0, 5)
|
||||||
.forEach(post => {
|
.forEach(post => {
|
||||||
var user = post.user();
|
const user = post.user();
|
||||||
suggestions.push(
|
suggestions.push(
|
||||||
makeSuggestion(user, '@'+user.username()+'#'+post.number(), [
|
makeSuggestion(user, '@' + user.username() + '#' + post.number(), [
|
||||||
'Reply to #', post.number(), ' — ',
|
app.trans('mentions.reply_to_post', {number: post.number()}), ' — ',
|
||||||
truncate(post.contentPlain(), 200)
|
truncate(post.contentPlain(), 200)
|
||||||
], 'suggestion-post')
|
], 'MentionsDropdown-post')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -119,7 +122,7 @@ export default function() {
|
||||||
if (user.username().toLowerCase().substr(0, typed.length) !== typed) return;
|
if (user.username().toLowerCase().substr(0, typed.length) !== typed) return;
|
||||||
|
|
||||||
suggestions.push(
|
suggestions.push(
|
||||||
makeSuggestion(user, '@'+user.username(), '', 'suggestion-user')
|
makeSuggestion(user, '@' + user.username(), '', 'MentionsDropdown-user')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -129,12 +132,12 @@ export default function() {
|
||||||
m.render($container[0], dropdown.render());
|
m.render($container[0], dropdown.render());
|
||||||
|
|
||||||
dropdown.show();
|
dropdown.show();
|
||||||
var coordinates = getCaretCoordinates(this, mentionStart);
|
const coordinates = getCaretCoordinates(this, mentionStart);
|
||||||
var left = coordinates.left;
|
const width = dropdown.$().outerWidth();
|
||||||
var top = coordinates.top + 15;
|
const height = dropdown.$().outerHeight();
|
||||||
var width = dropdown.$().outerWidth();
|
const parent = dropdown.$().offsetParent();
|
||||||
var height = dropdown.$().outerHeight();
|
let left = coordinates.left;
|
||||||
var parent = dropdown.$().offsetParent();
|
let top = coordinates.top + 15;
|
||||||
if (top + height > parent.height()) {
|
if (top + height > parent.height()) {
|
||||||
top = coordinates.top - height - 15;
|
top = coordinates.top - height - 15;
|
||||||
}
|
}
|
||||||
|
@ -149,15 +152,15 @@ export default function() {
|
||||||
|
|
||||||
dropdown.setIndex(0);
|
dropdown.setIndex(0);
|
||||||
dropdown.$().scrollTop(0);
|
dropdown.$().scrollTop(0);
|
||||||
dropdown.active(true);
|
dropdown.active = true;
|
||||||
|
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
if (typed) {
|
if (typed) {
|
||||||
searchTimeout = setTimeout(function() {
|
searchTimeout = setTimeout(function() {
|
||||||
var typedLower = typed.toLowerCase();
|
const typedLower = typed.toLowerCase();
|
||||||
if (searched.indexOf(typedLower) === -1) {
|
if (searched.indexOf(typedLower) === -1) {
|
||||||
app.store.find('users', {q: typed, page: {limit: 5}}).then(users => {
|
app.store.find('users', {q: typed, page: {limit: 5}}).then(() => {
|
||||||
if (dropdown.active()) buildSuggestions();
|
if (dropdown.active) buildSuggestions();
|
||||||
});
|
});
|
||||||
searched.push(typedLower);
|
searched.push(typedLower);
|
||||||
}
|
}
|
112
extensions/mentions/js/forum/src/addMentionedByList.js
Normal file
112
extensions/mentions/js/forum/src/addMentionedByList.js
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import { extend } from 'flarum/extend';
|
||||||
|
import Model from 'flarum/Model';
|
||||||
|
import Post from 'flarum/models/Post';
|
||||||
|
import CommentPost from 'flarum/components/CommentPost';
|
||||||
|
import PostPreview from 'flarum/components/PostPreview';
|
||||||
|
import punctuate from 'flarum/helpers/punctuate';
|
||||||
|
import username from 'flarum/helpers/username';
|
||||||
|
import icon from 'flarum/helpers/icon';
|
||||||
|
|
||||||
|
export default function addMentionedByList() {
|
||||||
|
Post.prototype.mentionedBy = Model.hasMany('mentionedBy');
|
||||||
|
|
||||||
|
extend(CommentPost.prototype, 'footerItems', function(items) {
|
||||||
|
const post = this.props.post;
|
||||||
|
const replies = post.mentionedBy();
|
||||||
|
|
||||||
|
if (replies && replies.length) {
|
||||||
|
// If there is only one reply, and it's adjacent to this post, we don't
|
||||||
|
// really need to show the list.
|
||||||
|
if (replies.length === 1 && replies[0].number() === post.number() + 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hidePreview = () => {
|
||||||
|
this.$('.Post-mentionedBy-preview')
|
||||||
|
.removeClass('in')
|
||||||
|
.one('transitionend', function() { $(this).hide(); });
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = function(element, isInitialized) {
|
||||||
|
if (isInitialized) return;
|
||||||
|
|
||||||
|
const $this = $(element);
|
||||||
|
let timeout;
|
||||||
|
|
||||||
|
const $preview = $('<ul class="Dropdown-menu Post-mentionedBy-preview fade"/>');
|
||||||
|
$this.append($preview);
|
||||||
|
|
||||||
|
$this.children().hover(function() {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(function() {
|
||||||
|
if (!$preview.hasClass('in') && $preview.is(':visible')) return;
|
||||||
|
|
||||||
|
// When the user hovers their mouse over the list of people who have
|
||||||
|
// replied to the post, render a list of reply previews into a
|
||||||
|
// popup.
|
||||||
|
m.render($preview[0], replies.map(reply => (
|
||||||
|
<li data-number={reply.number()}>
|
||||||
|
{PostPreview.component({
|
||||||
|
post: reply,
|
||||||
|
onclick: hidePreview
|
||||||
|
})}
|
||||||
|
</li>
|
||||||
|
)));
|
||||||
|
$preview.show();
|
||||||
|
setTimeout(() => $preview.off('transitionend').addClass('in'));
|
||||||
|
}, 500);
|
||||||
|
}, function() {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(hidePreview, 250);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Whenever the user hovers their mouse over a particular name in the
|
||||||
|
// list of repliers, highlight the corresponding post in the preview
|
||||||
|
// popup.
|
||||||
|
$this.find('.Post-mentionedBy-summary a').hover(function() {
|
||||||
|
$preview.find('[data-number="' + $(this).data('number') + '"]').addClass('active');
|
||||||
|
}, function() {
|
||||||
|
$preview.find('[data-number]').removeClass('active');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a list of unique users who have replied. So even if a user has
|
||||||
|
// replied twice, they will only be in this array once.
|
||||||
|
const used = [];
|
||||||
|
const repliers = replies.filter(reply => {
|
||||||
|
const user = reply.user();
|
||||||
|
const id = user && user.id();
|
||||||
|
if (used.indexOf(id) === -1) {
|
||||||
|
used.push(id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const names = repliers.sort(a => a === app.session.user ? -1 : 1)
|
||||||
|
.map(reply => {
|
||||||
|
const user = reply.user();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a href={app.route.post(reply)}
|
||||||
|
config={m.route}
|
||||||
|
onclick={hidePreview}
|
||||||
|
data-number={reply.number()}>
|
||||||
|
{app.session.user === user ? app.trans('mentions.you') : username(user)}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
items.add('replies',
|
||||||
|
<div className="Post-mentionedBy" config={config}>
|
||||||
|
<span className="Post-mentionedBy-summary">
|
||||||
|
{icon('reply')}
|
||||||
|
{app.trans('mentions.post_mentioned_by', {
|
||||||
|
count: names.length,
|
||||||
|
users: punctuate(names)
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,40 +1,46 @@
|
||||||
import { extend } from 'flarum/extension-utils';
|
import { extend } from 'flarum/extend';
|
||||||
import CommentPost from 'flarum/components/comment-post';
|
import CommentPost from 'flarum/components/CommentPost';
|
||||||
import PostPreview from 'flarum/components/post-preview';
|
import PostPreview from 'flarum/components/PostPreview';
|
||||||
import LoadingIndicator from 'flarum/components/loading-indicator';
|
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||||
|
|
||||||
export default function postMentionPreviews() {
|
export default function addPostMentionPreviews() {
|
||||||
extend(CommentPost.prototype, 'config', function() {
|
extend(CommentPost.prototype, 'config', function() {
|
||||||
var contentHtml = this.props.post.contentHtml();
|
const contentHtml = this.props.post.contentHtml();
|
||||||
|
|
||||||
if (contentHtml === this.oldPostContentHtml) return;
|
if (contentHtml === this.oldPostContentHtml) return;
|
||||||
|
|
||||||
this.oldPostContentHtml = contentHtml;
|
this.oldPostContentHtml = contentHtml;
|
||||||
|
|
||||||
var discussion = this.props.post.discussion();
|
const discussion = this.props.post.discussion();
|
||||||
|
|
||||||
this.$('.mention-post').each(function() {
|
this.$('.UserMention').each(function() {
|
||||||
var $this = $(this);
|
m.route.call(this, this, false, {}, {attrs: {href: this.getAttribute('href')}});
|
||||||
var number = $this.data('number');
|
});
|
||||||
var timeout;
|
|
||||||
|
this.$('.PostMention').each(function() {
|
||||||
|
const $this = $(this);
|
||||||
|
const number = $this.data('number');
|
||||||
|
let timeout;
|
||||||
|
|
||||||
// Wrap the mention link in a wrapper element so that we can insert a
|
// Wrap the mention link in a wrapper element so that we can insert a
|
||||||
// preview popup as its sibling and relatively position it.
|
// preview popup as its sibling and relatively position it.
|
||||||
var $preview = $('<ul class="dropdown-menu mention-post-preview fade"/>');
|
const $preview = $('<ul class="Dropdown-menu PostMention-preview fade"/>');
|
||||||
var $wrapper = $('<span class="mention-post-wrapper"/>');
|
const $wrapper = $('<span class="PostMention-wrapper"/>');
|
||||||
$this.wrap($wrapper).after($preview);
|
$this.wrap($wrapper).after($preview);
|
||||||
|
|
||||||
var getPostElement = function() {
|
const getPostElement = () => {
|
||||||
return $('.discussion-posts .item[data-number='+number+']');
|
return $(`.PostStream-item[data-number="${number}"]`);
|
||||||
};
|
};
|
||||||
|
|
||||||
var showPreview = function() {
|
const showPreview = () => {
|
||||||
// When the user hovers their mouse over the mention, look for the
|
// When the user hovers their mouse over the mention, look for the
|
||||||
// post that it's referring to in the stream, and determine if it's
|
// post that it's referring to in the stream, and determine if it's
|
||||||
// in the viewport. If it is, we will "pulsate" it.
|
// in the viewport. If it is, we will "pulsate" it.
|
||||||
var $post = getPostElement();
|
const $post = getPostElement();
|
||||||
var visible = false;
|
let visible = false;
|
||||||
if ($post.length) {
|
if ($post.length) {
|
||||||
var top = $post.offset().top;
|
const top = $post.offset().top;
|
||||||
var scrollTop = window.pageYOffset;
|
const scrollTop = window.pageYOffset;
|
||||||
if (top > scrollTop && top + $post.height() < scrollTop + $(window).height()) {
|
if (top > scrollTop && top + $post.height() < scrollTop + $(window).height()) {
|
||||||
$post.addClass('pulsate');
|
$post.addClass('pulsate');
|
||||||
visible = true;
|
visible = true;
|
||||||
|
@ -44,30 +50,33 @@ export default function postMentionPreviews() {
|
||||||
// Otherwise, we will show a popup preview of the post. If the post
|
// Otherwise, we will show a popup preview of the post. If the post
|
||||||
// hasn't yet been loaded, we will need to do that.
|
// hasn't yet been loaded, we will need to do that.
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
var showPost = function(post) {
|
|
||||||
m.render($preview[0], m('li', PostPreview.component({post})));
|
|
||||||
positionPreview();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Position the preview so that it appears above the mention.
|
// Position the preview so that it appears above the mention.
|
||||||
// (The offsetParent should be .post-body.)
|
// (The offsetParent should be .post-body.)
|
||||||
var positionPreview = function() {
|
const positionPreview = () => {
|
||||||
$preview.show().css('top', $this.offset().top - $this.offsetParent().offset().top - $preview.outerHeight(true));
|
$preview.show().css('top', $this.offset().top - $this.offsetParent().offset().top - $preview.outerHeight(true));
|
||||||
};
|
};
|
||||||
|
|
||||||
var post = discussion.posts().filter(post => post && post.number() == number)[0];
|
const showPost = post => {
|
||||||
|
m.render($preview[0], <li>{PostPreview.component({post})}</li>);
|
||||||
|
positionPreview();
|
||||||
|
};
|
||||||
|
|
||||||
|
const post = discussion.posts().filter(p => p && p.number() === number)[0];
|
||||||
if (post) {
|
if (post) {
|
||||||
showPost(post);
|
showPost(post);
|
||||||
} else {
|
} else {
|
||||||
m.render($preview[0], LoadingIndicator.component());
|
m.render($preview[0], LoadingIndicator.component());
|
||||||
app.store.find('posts', {discussions: discussion.id(), number}).then(posts => showPost(posts[0]));
|
app.store.find('posts', {
|
||||||
|
filter: {discussion: discussion.id(), number}
|
||||||
|
}).then(posts => showPost(posts[0]));
|
||||||
positionPreview();
|
positionPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => $preview.off('transitionend').addClass('in'));
|
setTimeout(() => $preview.off('transitionend').addClass('in'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var hidePreview = () => {
|
|
||||||
|
const hidePreview = () => {
|
||||||
getPostElement().removeClass('pulsate');
|
getPostElement().removeClass('pulsate');
|
||||||
if ($preview.hasClass('in')) {
|
if ($preview.hasClass('in')) {
|
||||||
$preview.removeClass('in').one('transitionend', () => $preview.hide());
|
$preview.removeClass('in').one('transitionend', () => $preview.hide());
|
||||||
|
@ -75,11 +84,11 @@ export default function postMentionPreviews() {
|
||||||
};
|
};
|
||||||
|
|
||||||
$this.parent().hover(
|
$this.parent().hover(
|
||||||
function() {
|
() => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = setTimeout(showPreview, 500);
|
timeout = setTimeout(showPreview, 500);
|
||||||
},
|
},
|
||||||
function() {
|
() => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
getPostElement().removeClass('pulsate');
|
getPostElement().removeClass('pulsate');
|
||||||
timeout = setTimeout(hidePreview, 250);
|
timeout = setTimeout(hidePreview, 250);
|
48
extensions/mentions/js/forum/src/addPostReplyAction.js
Normal file
48
extensions/mentions/js/forum/src/addPostReplyAction.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { extend } from 'flarum/extend';
|
||||||
|
import Button from 'flarum/components/Button';
|
||||||
|
import CommentPost from 'flarum/components/CommentPost';
|
||||||
|
import DiscussionControls from 'flarum/utils/DiscussionControls';
|
||||||
|
|
||||||
|
export default function() {
|
||||||
|
extend(CommentPost.prototype, 'actionItems', function(items) {
|
||||||
|
const post = this.props.post;
|
||||||
|
|
||||||
|
if (post.isHidden() || (app.session.user && !post.discussion().canReply())) return;
|
||||||
|
|
||||||
|
function insertMention(component, quote) {
|
||||||
|
const mention = '@' + post.user().username() + '#' + post.number() + ' ';
|
||||||
|
|
||||||
|
// If the composer is empty, then assume we're starting a new reply.
|
||||||
|
// In which case we don't want the user to have to confirm if they
|
||||||
|
// close the composer straight away.
|
||||||
|
if (!component.content()) {
|
||||||
|
component.props.originalContent = mention;
|
||||||
|
}
|
||||||
|
|
||||||
|
component.editor.insertAtCursor(
|
||||||
|
(component.editor.getSelectionRange()[0] > 0 ? '\n\n' : '') +
|
||||||
|
(quote
|
||||||
|
? '> ' + mention + quote.trim().replace(/\n/g, '\n> ') + '\n\n'
|
||||||
|
: mention)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.add('reply',
|
||||||
|
Button.component({
|
||||||
|
className: 'Button Button--text',
|
||||||
|
children: app.trans('mentions.reply_link'),
|
||||||
|
onclick: () => {
|
||||||
|
const quote = window.getSelection().toString();
|
||||||
|
|
||||||
|
const component = app.composer.component;
|
||||||
|
if (component && component.props.post && component.props.post.discussion() === post.discussion()) {
|
||||||
|
insertMention(component, quote);
|
||||||
|
} else {
|
||||||
|
DiscussionControls.replyAction.call(post.discussion())
|
||||||
|
.then(newComponent => insertMention(newComponent, quote));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
import Component from 'flarum/Component';
|
||||||
|
|
||||||
|
export default class AutocompleteDropdown extends Component {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
|
||||||
|
this.active = false;
|
||||||
|
this.index = 0;
|
||||||
|
this.keyWasJustPressed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
return (
|
||||||
|
<ul className="Dropdown-menu MentionsDropdown">
|
||||||
|
{this.props.items.map(item => <li>{item}</li>)}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
show(left, top) {
|
||||||
|
this.$().show().css({
|
||||||
|
left: left + 'px',
|
||||||
|
top: top + 'px'
|
||||||
|
});
|
||||||
|
this.active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.$().hide();
|
||||||
|
this.active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(e) {
|
||||||
|
if (!this.active) return;
|
||||||
|
|
||||||
|
switch (e.which) {
|
||||||
|
case 40: case 38: // Down/Up
|
||||||
|
this.keyWasJustPressed = true;
|
||||||
|
this.setIndex(this.index + (e.which === 40 ? 1 : -1), true);
|
||||||
|
clearTimeout(this.keyWasJustPressedTimeout);
|
||||||
|
this.keyWasJustPressedTimeout = setTimeout(() => this.keyWasJustPressed = false, 500);
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 13: case 9: // Enter/Tab
|
||||||
|
this.$('li').eq(this.index).find('button').click();
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 27: // Escape
|
||||||
|
this.hide();
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// no default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIndex(index, scrollToItem) {
|
||||||
|
if (this.keyWasJustPressed && !scrollToItem) return;
|
||||||
|
|
||||||
|
const $dropdown = this.$();
|
||||||
|
const $items = $dropdown.find('li');
|
||||||
|
let rangedIndex = index;
|
||||||
|
|
||||||
|
if (rangedIndex < 0) {
|
||||||
|
rangedIndex = $items.length - 1;
|
||||||
|
} else if (rangedIndex >= $items.length) {
|
||||||
|
rangedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.index = rangedIndex;
|
||||||
|
|
||||||
|
const $item = $items.removeClass('active').eq(rangedIndex).addClass('active');
|
||||||
|
|
||||||
|
if (scrollToItem) {
|
||||||
|
const dropdownScroll = $dropdown.scrollTop();
|
||||||
|
const dropdownTop = $dropdown.offset().top;
|
||||||
|
const dropdownBottom = dropdownTop + $dropdown.outerHeight();
|
||||||
|
const itemTop = $item.offset().top;
|
||||||
|
const itemBottom = itemTop + $item.outerHeight();
|
||||||
|
|
||||||
|
let scrollTop;
|
||||||
|
if (itemTop < dropdownTop) {
|
||||||
|
scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt($dropdown.css('padding-top'), 10);
|
||||||
|
} else if (itemBottom > dropdownBottom) {
|
||||||
|
scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt($dropdown.css('padding-bottom'), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof scrollTop !== 'undefined') {
|
||||||
|
$dropdown.stop(true).animate({scrollTop}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
import Notification from 'flarum/components/Notification';
|
||||||
|
import username from 'flarum/helpers/username';
|
||||||
|
import punctuate from 'flarum/helpers/punctuate';
|
||||||
|
|
||||||
|
export default class PostMentionedNotification extends Notification {
|
||||||
|
icon() {
|
||||||
|
return 'reply';
|
||||||
|
}
|
||||||
|
|
||||||
|
href() {
|
||||||
|
const notification = this.props.notification;
|
||||||
|
const post = notification.subject();
|
||||||
|
const auc = notification.additionalUnreadCount();
|
||||||
|
const content = notification.content();
|
||||||
|
|
||||||
|
return app.route.discussion(post.discussion(), auc ? post.number() : (content && content.replyNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
const notification = this.props.notification;
|
||||||
|
const post = notification.subject();
|
||||||
|
const auc = notification.additionalUnreadCount();
|
||||||
|
const user = notification.sender();
|
||||||
|
|
||||||
|
return app.trans('mentions.post_mentioned_notification', {
|
||||||
|
user,
|
||||||
|
username: auc ? punctuate([
|
||||||
|
username(user),
|
||||||
|
app.trans('mentions.others', {count: auc})
|
||||||
|
]) : undefined,
|
||||||
|
number: post.number()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import Notification from 'flarum/components/Notification';
|
||||||
|
import username from 'flarum/helpers/username';
|
||||||
|
|
||||||
|
export default class UserMentionedNotification extends Notification {
|
||||||
|
icon() {
|
||||||
|
return 'at';
|
||||||
|
}
|
||||||
|
|
||||||
|
href() {
|
||||||
|
const post = this.props.notification.subject();
|
||||||
|
|
||||||
|
return app.route.discussion(post.discussion(), post.number());
|
||||||
|
}
|
||||||
|
|
||||||
|
content() {
|
||||||
|
const user = this.props.notification.sender();
|
||||||
|
|
||||||
|
return app.trans('mentions.user_mentioned_notification', {user});
|
||||||
|
}
|
||||||
|
}
|
45
extensions/mentions/js/forum/src/main.js
Normal file
45
extensions/mentions/js/forum/src/main.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { extend } from 'flarum/extend';
|
||||||
|
import app from 'flarum/app';
|
||||||
|
import NotificationGrid from 'flarum/components/NotificationGrid';
|
||||||
|
|
||||||
|
import addPostMentionPreviews from 'mentions/addPostMentionPreviews';
|
||||||
|
import addMentionedByList from 'mentions/addMentionedByList';
|
||||||
|
import addPostReplyAction from 'mentions/addPostReplyAction';
|
||||||
|
import addComposerAutocomplete from 'mentions/addComposerAutocomplete';
|
||||||
|
import PostMentionedNotification from 'mentions/components/PostMentionedNotification';
|
||||||
|
import UserMentionedNotification from 'mentions/components/UserMentionedNotification';
|
||||||
|
|
||||||
|
app.initializers.add('mentions', function() {
|
||||||
|
// For every mention of a post inside a post's content, set up a hover handler
|
||||||
|
// that shows a preview of the mentioned post.
|
||||||
|
addPostMentionPreviews();
|
||||||
|
|
||||||
|
// In the footer of each post, show information about who has replied (i.e.
|
||||||
|
// who the post has been mentioned by).
|
||||||
|
addMentionedByList();
|
||||||
|
|
||||||
|
// Add a 'reply' control to the footer of each post. When clicked, it will
|
||||||
|
// open up the composer and add a post mention to its contents.
|
||||||
|
addPostReplyAction();
|
||||||
|
|
||||||
|
// After typing '@' in the composer, show a dropdown suggesting a bunch of
|
||||||
|
// posts or users that the user could mention.
|
||||||
|
addComposerAutocomplete();
|
||||||
|
|
||||||
|
app.notificationComponents.postMentioned = PostMentionedNotification;
|
||||||
|
app.notificationComponents.userMentioned = UserMentionedNotification;
|
||||||
|
|
||||||
|
// Add notification preferences.
|
||||||
|
extend(NotificationGrid.prototype, 'notificationTypes', function(items) {
|
||||||
|
items.add('postMentioned', {
|
||||||
|
name: 'postMentioned',
|
||||||
|
icon: 'reply',
|
||||||
|
label: 'Someone replies to my post'
|
||||||
|
});
|
||||||
|
items.add('userMentioned', {
|
||||||
|
name: 'userMentioned',
|
||||||
|
icon: 'at',
|
||||||
|
label: 'Someone mentions me in a post'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,89 +0,0 @@
|
||||||
import Component from 'flarum/component';
|
|
||||||
|
|
||||||
export default class AutocompleteDropdown extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.active = m.prop(false);
|
|
||||||
this.index = m.prop(0);
|
|
||||||
this.keyWasJustPressed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
view() {
|
|
||||||
return m('ul.dropdown-menu.mentions-dropdown', this.props.items.map(item => m('li', item)));
|
|
||||||
}
|
|
||||||
|
|
||||||
show(left, top) {
|
|
||||||
this.$().show().css({
|
|
||||||
left: left+'px',
|
|
||||||
top: top+'px'
|
|
||||||
});
|
|
||||||
this.active(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
hide() {
|
|
||||||
this.$().hide();
|
|
||||||
this.active(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate(e) {
|
|
||||||
if (!this.active()) return;
|
|
||||||
|
|
||||||
switch (e.which) {
|
|
||||||
case 40: case 38: // Down/Up
|
|
||||||
this.keyWasJustPressed = true;
|
|
||||||
this.setIndex(this.index() + (e.which === 40 ? 1 : -1), true);
|
|
||||||
clearTimeout(this.keyWasJustPressedTimeout);
|
|
||||||
this.keyWasJustPressedTimeout = setTimeout(() => this.keyWasJustPressed = false, 500);
|
|
||||||
e.preventDefault();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 13: case 9: // Enter/Tab
|
|
||||||
this.$('li').eq(this.index()).find('a').click();
|
|
||||||
e.preventDefault();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 27: // Escape
|
|
||||||
this.hide();
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIndex(index, scrollToItem) {
|
|
||||||
if (this.keyWasJustPressed && !scrollToItem) return;
|
|
||||||
|
|
||||||
var $dropdown = this.$();
|
|
||||||
var $items = $dropdown.find('li');
|
|
||||||
|
|
||||||
if (index < 0) {
|
|
||||||
index = $items.length - 1;
|
|
||||||
} else if (index >= $items.length) {
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.index(index);
|
|
||||||
|
|
||||||
var $item = $items.removeClass('active').eq(index).addClass('active');
|
|
||||||
|
|
||||||
if (scrollToItem) {
|
|
||||||
var dropdownScroll = $dropdown.scrollTop();
|
|
||||||
var dropdownTop = $dropdown.offset().top;
|
|
||||||
var dropdownBottom = dropdownTop + $dropdown.outerHeight();
|
|
||||||
var itemTop = $item.offset().top;
|
|
||||||
var itemBottom = itemTop + $item.outerHeight();
|
|
||||||
|
|
||||||
var scrollTop;
|
|
||||||
if (itemTop < dropdownTop) {
|
|
||||||
scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt($dropdown.css('padding-top'));
|
|
||||||
} else if (itemBottom > dropdownBottom) {
|
|
||||||
scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt($dropdown.css('padding-bottom'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof scrollTop !== 'undefined') {
|
|
||||||
$dropdown.stop(true).animate({scrollTop}, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
import Notification from 'flarum/components/notification';
|
|
||||||
import username from 'flarum/helpers/username';
|
|
||||||
|
|
||||||
export default class PostMentionedNotification extends Notification {
|
|
||||||
view() {
|
|
||||||
var notification = this.props.notification;
|
|
||||||
var post = notification.subject();
|
|
||||||
var auc = notification.additionalUnreadCount();
|
|
||||||
var content = notification.content();
|
|
||||||
|
|
||||||
return super.view({
|
|
||||||
href: app.route.discussion(post.discussion(), auc ? post.number() : (content && content.replyNumber)),
|
|
||||||
icon: 'reply',
|
|
||||||
content: [username(notification.sender()), (auc ? ' and '+auc+' others' : '')+' replied to your post #'+post.number()]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import Notification from 'flarum/components/notification';
|
|
||||||
import username from 'flarum/helpers/username';
|
|
||||||
|
|
||||||
export default class UserMentionedNotification extends Notification {
|
|
||||||
view() {
|
|
||||||
var notification = this.props.notification;
|
|
||||||
var post = notification.subject();
|
|
||||||
|
|
||||||
return super.view({
|
|
||||||
href: app.route.discussion(post.discussion(), post.number()),
|
|
||||||
icon: 'at',
|
|
||||||
content: [username(notification.sender()), ' mentioned you']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,102 +0,0 @@
|
||||||
import { extend } from 'flarum/extension-utils';
|
|
||||||
import Model from 'flarum/model';
|
|
||||||
import Post from 'flarum/models/post';
|
|
||||||
import DiscussionPage from 'flarum/components/discussion-page';
|
|
||||||
import CommentPost from 'flarum/components/comment-post';
|
|
||||||
import PostPreview from 'flarum/components/post-preview';
|
|
||||||
import punctuate from 'flarum/helpers/punctuate';
|
|
||||||
import username from 'flarum/helpers/username';
|
|
||||||
import icon from 'flarum/helpers/icon';
|
|
||||||
|
|
||||||
export default function mentionedByList() {
|
|
||||||
Post.prototype.mentionedBy = Model.many('mentionedBy');
|
|
||||||
|
|
||||||
extend(DiscussionPage.prototype, 'params', function(params) {
|
|
||||||
params.include.push('posts.mentionedBy', 'posts.mentionedBy.user');
|
|
||||||
});
|
|
||||||
|
|
||||||
extend(CommentPost.prototype, 'footerItems', function(items) {
|
|
||||||
var post = this.props.post;
|
|
||||||
var replies = post.mentionedBy();
|
|
||||||
if (replies && replies.length) {
|
|
||||||
|
|
||||||
// If there is only one reply, and it's adjacent to this post, we don't
|
|
||||||
// really need to show the list.
|
|
||||||
if (replies.length === 1 && replies[0].number() == post.number() + 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hidePreview = () => {
|
|
||||||
this.$('.mentioned-by-preview').removeClass('in').one('transitionend', function() { $(this).hide(); });
|
|
||||||
};
|
|
||||||
|
|
||||||
var config = function(element, isInitialized) {
|
|
||||||
if (isInitialized) return;
|
|
||||||
var $this = $(element);
|
|
||||||
var timeout;
|
|
||||||
|
|
||||||
var $preview = $('<ul class="dropdown-menu mentioned-by-preview fade"/>');
|
|
||||||
$this.append($preview);
|
|
||||||
|
|
||||||
$this.children().hover(function() {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(function() {
|
|
||||||
if (!$preview.hasClass('in') && $preview.is(':visible')) return;
|
|
||||||
|
|
||||||
// When the user hovers their mouse over the list of people who have
|
|
||||||
// replied to the post, render a list of reply previews into a
|
|
||||||
// popup.
|
|
||||||
m.render($preview[0], replies.map(post => {
|
|
||||||
return m('li', {'data-number': post.number()}, PostPreview.component({post, onclick: hidePreview}));
|
|
||||||
}));
|
|
||||||
$preview.show();
|
|
||||||
setTimeout(() => $preview.off('transitionend').addClass('in'));
|
|
||||||
}, 500);
|
|
||||||
}, function() {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(hidePreview, 250);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Whenever the user hovers their mouse over a particular name in the
|
|
||||||
// list of repliers, highlight the corresponding post in the preview
|
|
||||||
// popup.
|
|
||||||
$this.find('.summary a').hover(function() {
|
|
||||||
$preview.find('[data-number='+$(this).data('number')+']').addClass('active');
|
|
||||||
}, function() {
|
|
||||||
$preview.find('[data-number]').removeClass('active');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a list of unique users who have replied. So even if a user has
|
|
||||||
// replied twice, they will only be in this array once.
|
|
||||||
var used = [];
|
|
||||||
var repliers = replies.filter(reply => {
|
|
||||||
var user = reply.user();
|
|
||||||
var id = user && user.id();
|
|
||||||
if (used.indexOf(id) === -1) {
|
|
||||||
used.push(id);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
items.add('replies',
|
|
||||||
m('div.mentioned-by', {config}, [
|
|
||||||
m('span.summary', [
|
|
||||||
icon('reply icon'),
|
|
||||||
punctuate(repliers.map(reply => {
|
|
||||||
return m('a', {
|
|
||||||
href: app.route.post(reply),
|
|
||||||
config: m.route,
|
|
||||||
onclick: hidePreview,
|
|
||||||
'data-number': reply.number()
|
|
||||||
}, [
|
|
||||||
app.session.user() && reply.user() === app.session.user() ? 'You' : username(reply.user())
|
|
||||||
])
|
|
||||||
})),
|
|
||||||
' replied to this.'
|
|
||||||
])
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { extend } from 'flarum/extension-utils';
|
|
||||||
import ActionButton from 'flarum/components/action-button';
|
|
||||||
import CommentPost from 'flarum/components/comment-post';
|
|
||||||
|
|
||||||
export default function() {
|
|
||||||
extend(CommentPost.prototype, 'actionItems', function(items) {
|
|
||||||
var post = this.props.post;
|
|
||||||
if (post.isHidden() || (app.session.user() && !post.discussion().canReply())) return;
|
|
||||||
|
|
||||||
function insertMention(component, quote) {
|
|
||||||
var mention = '@'+post.user().username()+'#'+post.number()+' ';
|
|
||||||
|
|
||||||
// If the composer is empty, then assume we're starting a new reply.
|
|
||||||
// In which case we don't want the user to have to confirm if they
|
|
||||||
// close the composer straight away.
|
|
||||||
if (!component.content()) {
|
|
||||||
component.props.originalContent = mention;
|
|
||||||
}
|
|
||||||
|
|
||||||
component.editor.insertAtCursor((component.editor.getSelectionRange()[0] > 0 ? '\n\n' : '')+(quote ? '> '+mention+quote.trim().replace(/\n/g, '\n> ')+'\n\n' : mention));
|
|
||||||
}
|
|
||||||
|
|
||||||
items.add('reply',
|
|
||||||
ActionButton.component({
|
|
||||||
icon: 'reply',
|
|
||||||
label: 'Reply',
|
|
||||||
onclick: () => {
|
|
||||||
var quote = window.getSelection().toString();
|
|
||||||
|
|
||||||
var component = app.composer.component;
|
|
||||||
if (component && component.props.post && component.props.post.discussion() === post.discussion()) {
|
|
||||||
insertMention(component, quote);
|
|
||||||
} else {
|
|
||||||
post.discussion().replyAction().then(component => insertMention(component, quote));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
83
extensions/mentions/less/forum/extension.less
Normal file
83
extensions/mentions/less/forum/extension.less
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
.PostMention, .UserMention {
|
||||||
|
background: @control-bg;
|
||||||
|
color: @control-color;
|
||||||
|
border-radius: @border-radius;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border: 0 !important;
|
||||||
|
|
||||||
|
blockquote & {
|
||||||
|
background: @body-bg;
|
||||||
|
}
|
||||||
|
&:hover,
|
||||||
|
&:active {
|
||||||
|
color: @link-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.PostMention {
|
||||||
|
margin: 0 3px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
.fa();
|
||||||
|
content: @fa-var-reply;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.TextEditor {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.MentionsDropdown {
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: auto;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
mark {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
> li > a:hover {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.MentionsDropdown, .PostMention-preview, .Post-mentionedBy-preview {
|
||||||
|
.PostPreview {
|
||||||
|
color: @muted-color;
|
||||||
|
|
||||||
|
.Avatar {
|
||||||
|
.Avatar--size(24px);
|
||||||
|
margin: 0 0 0 -37px;
|
||||||
|
|
||||||
|
.MentionsDropdown-post& {
|
||||||
|
margin-top: 3px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.username {
|
||||||
|
color: @text-color;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.PostPreview-content {
|
||||||
|
padding-left: 37px;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.7em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.Post-mentionedBy {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.Post-mentionedBy-summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.Post-mentionedBy-preview, .PostMention-preview, .MentionsDropdown {
|
||||||
|
margin: 5px 0 !important;
|
||||||
|
|
||||||
|
> li > a {
|
||||||
|
white-space: normal;
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,77 +0,0 @@
|
||||||
.mention-post, .mention-user {
|
|
||||||
background: @fl-body-control-bg;
|
|
||||||
color: @fl-body-control-color;
|
|
||||||
border-radius: @border-radius-base;
|
|
||||||
padding: 2px 5px;
|
|
||||||
border: 0 !important;
|
|
||||||
|
|
||||||
blockquote & {
|
|
||||||
background: @fl-body-bg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.mention-post {
|
|
||||||
margin: 0 3px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
.fa();
|
|
||||||
content: @fa-var-reply;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.text-editor {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.mentions-dropdown {
|
|
||||||
max-width: 500px;
|
|
||||||
max-height: 200px;
|
|
||||||
overflow: auto;
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
& mark {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
& > li > a:hover {
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.post-preview {
|
|
||||||
color: @fl-body-muted-color !important;
|
|
||||||
|
|
||||||
& .avatar {
|
|
||||||
.avatar-size(24px);
|
|
||||||
margin: 0 0 0 -37px;
|
|
||||||
|
|
||||||
.suggestion-post& {
|
|
||||||
margin-top: 3px;
|
|
||||||
margin-bottom: 3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
& .username {
|
|
||||||
color: @fl-body-color;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.post-preview-content {
|
|
||||||
padding-left: 37px;
|
|
||||||
overflow: hidden;
|
|
||||||
line-height: 1.7em;
|
|
||||||
}
|
|
||||||
.mentioned-by {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
& .summary {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.mentioned-by-preview, .mention-post-preview, .mentions-dropdown {
|
|
||||||
margin: 5px 0 !important;
|
|
||||||
|
|
||||||
& > li > a {
|
|
||||||
white-space: normal;
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
8
extensions/mentions/locale/en.yml
Normal file
8
extensions/mentions/locale/en.yml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
mentions:
|
||||||
|
reply_to_post: "Reply to #{number}"
|
||||||
|
post_mentioned_notification: "{username} replied to your post #{number}"
|
||||||
|
others: "{count} others"
|
||||||
|
user_mentioned_notification: "{username} mentioned you"
|
||||||
|
post_mentioned_by: "{users} replied to this."
|
||||||
|
you: You
|
||||||
|
reply_link: Reply
|
18
extensions/mentions/src/Extension.php
Normal file
18
extensions/mentions/src/Extension.php
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?php namespace Flarum\Mentions;
|
||||||
|
|
||||||
|
use Flarum\Support\Extension as BaseExtension;
|
||||||
|
use Illuminate\Events\Dispatcher;
|
||||||
|
|
||||||
|
class Extension extends BaseExtension
|
||||||
|
{
|
||||||
|
public function boot(Dispatcher $events)
|
||||||
|
{
|
||||||
|
$events->subscribe('Flarum\Mentions\Listeners\AddClientAssets');
|
||||||
|
$events->subscribe('Flarum\Mentions\Listeners\AddModelRelationships');
|
||||||
|
$events->subscribe('Flarum\Mentions\Listeners\AddApiRelationships');
|
||||||
|
$events->subscribe('Flarum\Mentions\Listeners\AddUserMentionsFormatter');
|
||||||
|
$events->subscribe('Flarum\Mentions\Listeners\AddPostMentionsFormatter');
|
||||||
|
$events->subscribe('Flarum\Mentions\Listeners\UpdateUserMentionsMetadata');
|
||||||
|
$events->subscribe('Flarum\Mentions\Listeners\UpdatePostMentionsMetadata');
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,94 +0,0 @@
|
||||||
<?php namespace Flarum\Mentions\Handlers;
|
|
||||||
|
|
||||||
use Flarum\Mentions\PostMentionsParser;
|
|
||||||
use Flarum\Mentions\PostMentionedActivity;
|
|
||||||
use Flarum\Mentions\PostMentionedNotification;
|
|
||||||
use Flarum\Core\Activity\ActivitySyncer;
|
|
||||||
use Flarum\Core\Notifications\NotificationSyncer;
|
|
||||||
use Flarum\Core\Events\PostWasPosted;
|
|
||||||
use Flarum\Core\Events\PostWasRevised;
|
|
||||||
use Flarum\Core\Events\PostWasHidden;
|
|
||||||
use Flarum\Core\Events\PostWasRestored;
|
|
||||||
use Flarum\Core\Events\PostWasDeleted;
|
|
||||||
use Flarum\Core\Models\Post;
|
|
||||||
use Flarum\Core\Notifications\Notifier;
|
|
||||||
use Illuminate\Contracts\Events\Dispatcher;
|
|
||||||
|
|
||||||
class PostMentionsMetadataUpdater
|
|
||||||
{
|
|
||||||
protected $parser;
|
|
||||||
|
|
||||||
protected $activity;
|
|
||||||
|
|
||||||
protected $notifications;
|
|
||||||
|
|
||||||
public function __construct(PostMentionsParser $parser, ActivitySyncer $activity, NotificationSyncer $notifications)
|
|
||||||
{
|
|
||||||
$this->parser = $parser;
|
|
||||||
$this->activity = $activity;
|
|
||||||
$this->notifications = $notifications;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function subscribe(Dispatcher $events)
|
|
||||||
{
|
|
||||||
$events->listen('Flarum\Core\Events\PostWasPosted', __CLASS__.'@whenPostWasPosted');
|
|
||||||
$events->listen('Flarum\Core\Events\PostWasRevised', __CLASS__.'@whenPostWasRevised');
|
|
||||||
$events->listen('Flarum\Core\Events\PostWasHidden', __CLASS__.'@whenPostWasHidden');
|
|
||||||
$events->listen('Flarum\Core\Events\PostWasRestored', __CLASS__.'@whenPostWasRestored');
|
|
||||||
$events->listen('Flarum\Core\Events\PostWasDeleted', __CLASS__.'@whenPostWasDeleted');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function whenPostWasPosted(PostWasPosted $event)
|
|
||||||
{
|
|
||||||
$this->replyBecameVisible($event->post);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function whenPostWasRevised(PostWasRevised $event)
|
|
||||||
{
|
|
||||||
$this->replyBecameVisible($event->post);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function whenPostWasHidden(PostWasHidden $event)
|
|
||||||
{
|
|
||||||
$this->replyBecameInvisible($event->post);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function whenPostWasRestored(PostWasRestored $event)
|
|
||||||
{
|
|
||||||
$this->replyBecameVisible($event->post);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function whenPostWasDeleted(PostWasDeleted $event)
|
|
||||||
{
|
|
||||||
$this->replyBecameInvisible($event->post);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function replyBecameVisible(Post $reply)
|
|
||||||
{
|
|
||||||
$matches = $this->parser->match($reply->content);
|
|
||||||
|
|
||||||
$mentioned = $reply->discussion->posts()->with('user')->whereIn('number', array_filter($matches['number']))->get()->all();
|
|
||||||
|
|
||||||
$this->sync($reply, $mentioned);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function replyBecameInvisible(Post $reply)
|
|
||||||
{
|
|
||||||
$this->sync($reply, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function sync(Post $reply, array $mentioned)
|
|
||||||
{
|
|
||||||
$reply->mentionsPosts()->sync(array_pluck($mentioned, 'id'));
|
|
||||||
|
|
||||||
$mentioned = array_filter($mentioned, function ($post) use ($reply) {
|
|
||||||
return $post->user->id !== $reply->user->id;
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach ($mentioned as $post) {
|
|
||||||
$this->activity->sync(new PostMentionedActivity($post, $reply), [$post->user]);
|
|
||||||
|
|
||||||
$this->notifications->sync(new PostMentionedNotification($post, $reply), [$post->user]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
<?php namespace Flarum\Mentions\Handlers;
|
|
||||||
|
|
||||||
use Flarum\Mentions\UserMentionsParser;
|
|
||||||
use Flarum\Mentions\UserMentionedActivity;
|
|
||||||
use Flarum\Mentions\UserMentionedNotification;
|
|
||||||
use Flarum\Core\Activity\ActivitySyncer;
|
|
||||||
use Flarum\Core\Notifications\NotificationSyncer;
|
|
||||||
use Flarum\Core\Events\PostWasPosted;
|
|
||||||
use Flarum\Core\Events\PostWasRevised;
|
|
||||||
use Flarum\Core\Events\PostWasDeleted;
|
|
||||||
use Flarum\Core\Events\PostWasHidden;
|
|
||||||
use Flarum\Core\Events\PostWasRestored;
|
|
||||||
use Flarum\Core\Models\Post;
|
|
||||||
use Flarum\Core\Models\User;
|
|
||||||
use Illuminate\Contracts\Events\Dispatcher;
|
|
||||||
|
|
||||||
class UserMentionsMetadataUpdater
|
|
||||||
{
|
|
||||||
protected $parser;
|
|
||||||
|
|
||||||
protected $activity;
|
|
||||||
|
|
||||||
protected $notifications;
|
|
||||||
|
|
||||||
public function __construct(UserMentionsParser $parser, ActivitySyncer $activity, NotificationSyncer $notifications)
|
|
||||||
{
|
|
||||||
$this->parser = $parser;
|
|
||||||
$this->activity = $activity;
|
|
||||||
$this->notifications = $notifications;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function subscribe(Dispatcher $events)
|
|
||||||
{
|
|
||||||
$events->listen('Flarum\Core\Events\PostWasPosted', __CLASS__.'@whenPostWasPosted');
|
|
||||||
$events->listen('Flarum\Core\Events\PostWasRevised', __CLASS__.'@whenPostWasRevised');
|
|
||||||
$events->listen('Flarum\Core\Events\PostWasHidden', __CLASS__.'@whenPostWasHidden');
|
|
||||||
$events->listen('Flarum\Core\Events\PostWasRestored', __CLASS__.'@whenPostWasRestored');
|
|
||||||
$events->listen('Flarum\Core\Events\PostWasDeleted', __CLASS__.'@whenPostWasDeleted');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function whenPostWasPosted(PostWasPosted $event)
|
|
||||||
{
|
|
||||||
$this->postBecameVisible($event->post);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function whenPostWasRevised(PostWasRevised $event)
|
|
||||||
{
|
|
||||||
$this->postBecameVisible($event->post);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function whenPostWasHidden(PostWasHidden $event)
|
|
||||||
{
|
|
||||||
$this->postBecameInvisible($event->post);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function whenPostWasRestored(PostWasRestored $event)
|
|
||||||
{
|
|
||||||
$this->postBecameVisible($event->post);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function whenPostWasDeleted(PostWasDeleted $event)
|
|
||||||
{
|
|
||||||
$this->postBecameInvisible($event->post);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function postBecameVisible(Post $post)
|
|
||||||
{
|
|
||||||
$matches = $this->parser->match($post->content);
|
|
||||||
|
|
||||||
$mentioned = User::whereIn('username', array_filter($matches['username']))->get()->all();
|
|
||||||
|
|
||||||
$this->sync($post, $mentioned);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function postBecameInvisible(Post $post)
|
|
||||||
{
|
|
||||||
$this->sync($post, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function sync(Post $post, array $mentioned)
|
|
||||||
{
|
|
||||||
$post->mentionsUsers()->sync(array_pluck($mentioned, 'id'));
|
|
||||||
|
|
||||||
$mentioned = array_filter($mentioned, function ($user) use ($post) {
|
|
||||||
return $user->id !== $post->user->id;
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->activity->sync(new UserMentionedActivity($post), $mentioned);
|
|
||||||
|
|
||||||
$this->notifications->sync(new UserMentionedNotification($post), $mentioned);
|
|
||||||
}
|
|
||||||
}
|
|
55
extensions/mentions/src/Listeners/AddApiRelationships.php
Executable file
55
extensions/mentions/src/Listeners/AddApiRelationships.php
Executable file
|
@ -0,0 +1,55 @@
|
||||||
|
<?php namespace Flarum\Mentions\Listeners;
|
||||||
|
|
||||||
|
use Flarum\Events\ApiRelationship;
|
||||||
|
use Flarum\Events\BuildApiAction;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use Flarum\Api\Serializers\PostBasicSerializer;
|
||||||
|
use Flarum\Api\Actions\Discussions;
|
||||||
|
use Flarum\Api\Actions\Posts;
|
||||||
|
|
||||||
|
class AddApiRelationships
|
||||||
|
{
|
||||||
|
public function subscribe(Dispatcher $events)
|
||||||
|
{
|
||||||
|
$events->listen(ApiRelationship::class, __CLASS__.'@addRelationships');
|
||||||
|
$events->listen(BuildApiAction::class, __CLASS__.'@includeRelationships');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addRelationships(ApiRelationship $event)
|
||||||
|
{
|
||||||
|
if ($event->serializer instanceof PostBasicSerializer) {
|
||||||
|
if ($event->relationship === 'mentionedBy') {
|
||||||
|
return $event->serializer->hasMany('Flarum\Api\Serializers\PostBasicSerializer', 'mentionedBy');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($event->relationship === 'mentionsPosts') {
|
||||||
|
return $event->serializer->hasMany('Flarum\Api\Serializers\PostBasicSerializer', 'mentionsPosts');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($event->relationship === 'mentionsUsers') {
|
||||||
|
return $event->serializer->hasMany('Flarum\Api\Serializers\PostBasicSerializer', 'mentionsUsers');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function includeRelationships(BuildApiAction $event)
|
||||||
|
{
|
||||||
|
if ($event->action instanceof Discussions\ShowAction) {
|
||||||
|
$event->addInclude('posts.mentionedBy');
|
||||||
|
$event->addInclude('posts.mentionedBy.user');
|
||||||
|
$event->addLink('posts.mentionedBy.discussion');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($event->action instanceof Posts\ShowAction ||
|
||||||
|
$event->action instanceof Posts\IndexAction) {
|
||||||
|
$event->addInclude('mentionedBy');
|
||||||
|
$event->addInclude('mentionedBy.user');
|
||||||
|
$event->addLink('mentionedBy.discussion');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($event->action instanceof Posts\CreateAction) {
|
||||||
|
$event->addInclude('mentionsPosts');
|
||||||
|
$event->addInclude('mentionsPosts.mentionedBy');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
extensions/mentions/src/Listeners/AddClientAssets.php
Executable file
39
extensions/mentions/src/Listeners/AddClientAssets.php
Executable file
|
@ -0,0 +1,39 @@
|
||||||
|
<?php namespace Flarum\Mentions\Listeners;
|
||||||
|
|
||||||
|
use Flarum\Events\RegisterLocales;
|
||||||
|
use Flarum\Events\BuildClientView;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
|
||||||
|
class AddClientAssets
|
||||||
|
{
|
||||||
|
public function subscribe(Dispatcher $events)
|
||||||
|
{
|
||||||
|
$events->listen(RegisterLocales::class, __CLASS__.'@addLocale');
|
||||||
|
$events->listen(BuildClientView::class, __CLASS__.'@addAssets');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addLocale(RegisterLocales $event)
|
||||||
|
{
|
||||||
|
$event->addTranslations('en', __DIR__.'/../../locale/en.yml');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addAssets(BuildClientView $event)
|
||||||
|
{
|
||||||
|
$event->forumAssets([
|
||||||
|
__DIR__.'/../../js/forum/dist/extension.js',
|
||||||
|
__DIR__.'/../../less/forum/extension.less'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$event->forumBootstrapper('mentions/main');
|
||||||
|
|
||||||
|
$event->forumTranslations([
|
||||||
|
'mentions.reply_to_post',
|
||||||
|
'mentions.post_mentioned_notification',
|
||||||
|
'mentions.others',
|
||||||
|
'mentions.user_mentioned_notification',
|
||||||
|
'mentions.post_mentioned_by',
|
||||||
|
'mentions.you',
|
||||||
|
'mentions.reply_link'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
30
extensions/mentions/src/Listeners/AddModelRelationships.php
Executable file
30
extensions/mentions/src/Listeners/AddModelRelationships.php
Executable file
|
@ -0,0 +1,30 @@
|
||||||
|
<?php namespace Flarum\Mentions\Listeners;
|
||||||
|
|
||||||
|
use Flarum\Events\ModelRelationship;
|
||||||
|
use Flarum\Core\Posts\Post;
|
||||||
|
use Flarum\Core\Users\User;
|
||||||
|
|
||||||
|
class AddModelRelationships
|
||||||
|
{
|
||||||
|
public function subscribe($events)
|
||||||
|
{
|
||||||
|
$events->listen(ModelRelationship::class, __CLASS__.'@addRelationships');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addRelationships(ModelRelationship $event)
|
||||||
|
{
|
||||||
|
if ($event->model instanceof Post) {
|
||||||
|
if ($event->relationship === 'mentionedBy') {
|
||||||
|
return $event->model->belongsToMany(Post::class, 'mentions_posts', 'mentions_id', 'post_id', 'mentionedBy');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($event->relationship === 'mentionsPosts') {
|
||||||
|
return $event->model->belongsToMany(Post::class, 'mentions_posts', 'post_id', 'mentions_id', 'mentionsPosts');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($event->relationship === 'mentionsUsers') {
|
||||||
|
return $event->model->belongsToMany(User::class, 'mentions_users', 'post_id', 'mentions_id', 'mentionsUsers');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
extensions/mentions/src/Listeners/AddPostMentionsFormatter.php
Executable file
47
extensions/mentions/src/Listeners/AddPostMentionsFormatter.php
Executable file
|
@ -0,0 +1,47 @@
|
||||||
|
<?php namespace Flarum\Mentions\Listeners;
|
||||||
|
|
||||||
|
use Flarum\Events\FormatterConfigurator;
|
||||||
|
use Flarum\Events\FormatterRenderer;
|
||||||
|
use Flarum\Core\Posts\CommentPost;
|
||||||
|
|
||||||
|
class AddPostMentionsFormatter
|
||||||
|
{
|
||||||
|
public function subscribe($events)
|
||||||
|
{
|
||||||
|
$events->listen(FormatterConfigurator::class, __CLASS__.'@configure');
|
||||||
|
$events->listen(FormatterRenderer::class, __CLASS__.'@render');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configure(FormatterConfigurator $event)
|
||||||
|
{
|
||||||
|
$configurator = $event->configurator;
|
||||||
|
|
||||||
|
$tagName = 'POSTMENTION';
|
||||||
|
|
||||||
|
$tag = $configurator->tags->add($tagName);
|
||||||
|
$tag->attributes->add('username');
|
||||||
|
$tag->attributes->add('number');
|
||||||
|
$tag->attributes->add('id')->filterChain->append('#uint');
|
||||||
|
$tag->template = '<a href="{$DISCUSSION_URL}{@number}" class="PostMention" data-number="{@number}"><xsl:value-of select="@username"/></a>';
|
||||||
|
$tag->filterChain->prepend([static::class, 'addId'])->addParameterByName('post');
|
||||||
|
|
||||||
|
$configurator->Preg->match('/\B@(?<username>[a-z0-9_-]+)#(?<number>\d+)/i', $tagName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(FormatterRenderer $event)
|
||||||
|
{
|
||||||
|
// TODO: use URL generator
|
||||||
|
$event->renderer->setParameter('DISCUSSION_URL', '/d/' . $event->post->discussion_id . '/-/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function addId($tag, CommentPost $post)
|
||||||
|
{
|
||||||
|
$id = CommentPost::where('discussion_id', $post->discussion_id)
|
||||||
|
->where('number', $tag->getAttribute('number'))
|
||||||
|
->pluck('id');
|
||||||
|
|
||||||
|
$tag->setAttribute('id', $id);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
56
extensions/mentions/src/Listeners/AddUserMentionsFormatter.php
Executable file
56
extensions/mentions/src/Listeners/AddUserMentionsFormatter.php
Executable file
|
@ -0,0 +1,56 @@
|
||||||
|
<?php namespace Flarum\Mentions\Listeners;
|
||||||
|
|
||||||
|
use Flarum\Events\FormatterConfigurator;
|
||||||
|
use Flarum\Events\FormatterRenderer;
|
||||||
|
use Flarum\Events\FormatterParser;
|
||||||
|
use Flarum\Core\Users\UserRepository;
|
||||||
|
|
||||||
|
class AddUserMentionsFormatter
|
||||||
|
{
|
||||||
|
protected $users;
|
||||||
|
|
||||||
|
public function __construct(UserRepository $users)
|
||||||
|
{
|
||||||
|
$this->users = $users;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function subscribe($events)
|
||||||
|
{
|
||||||
|
$events->listen(FormatterConfigurator::class, __CLASS__.'@configure');
|
||||||
|
$events->listen(FormatterParser::class, __CLASS__.'@parse');
|
||||||
|
$events->listen(FormatterRenderer::class, __CLASS__.'@render');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configure(FormatterConfigurator $event)
|
||||||
|
{
|
||||||
|
$configurator = $event->configurator;
|
||||||
|
|
||||||
|
$tagName = 'USERMENTION';
|
||||||
|
|
||||||
|
$tag = $configurator->tags->add($tagName);
|
||||||
|
$tag->attributes->add('username');
|
||||||
|
$tag->attributes->add('id')->filterChain->append('#uint');
|
||||||
|
$tag->template = '<a href="{$PROFILE_URL}{@username}" class="UserMention">@<xsl:value-of select="@username"/></a>';
|
||||||
|
$tag->filterChain->prepend([static::class, 'addId'])->addParameterByName('userRepository');
|
||||||
|
|
||||||
|
$configurator->Preg->match('/\B@(?<username>[a-z0-9_-]+)(?!#)/i', $tagName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function parse(FormatterParser $event)
|
||||||
|
{
|
||||||
|
$event->parser->registeredVars['userRepository'] = $this->users;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(FormatterRenderer $event)
|
||||||
|
{
|
||||||
|
// TODO: use URL generator
|
||||||
|
$event->renderer->setParameter('PROFILE_URL', '/u/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function addId($tag, UserRepository $users)
|
||||||
|
{
|
||||||
|
$tag->setAttribute('id', $users->getIdForUsername($tag->getAttribute('username')));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
97
extensions/mentions/src/Listeners/UpdatePostMentionsMetadata.php
Executable file
97
extensions/mentions/src/Listeners/UpdatePostMentionsMetadata.php
Executable file
|
@ -0,0 +1,97 @@
|
||||||
|
<?php namespace Flarum\Mentions\Listeners;
|
||||||
|
|
||||||
|
use Flarum\Mentions\Notifications\PostMentionedBlueprint;
|
||||||
|
use Flarum\Core\Notifications\NotificationSyncer;
|
||||||
|
use Flarum\Events\RegisterNotificationTypes;
|
||||||
|
use Flarum\Events\PostWasPosted;
|
||||||
|
use Flarum\Events\PostWasRevised;
|
||||||
|
use Flarum\Events\PostWasHidden;
|
||||||
|
use Flarum\Events\PostWasRestored;
|
||||||
|
use Flarum\Events\PostWasDeleted;
|
||||||
|
use Flarum\Core\Posts\Post;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use s9e\TextFormatter\Utils;
|
||||||
|
|
||||||
|
class UpdatePostMentionsMetadata
|
||||||
|
{
|
||||||
|
protected $notifications;
|
||||||
|
|
||||||
|
public function __construct(NotificationSyncer $notifications)
|
||||||
|
{
|
||||||
|
$this->notifications = $notifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function subscribe(Dispatcher $events)
|
||||||
|
{
|
||||||
|
$events->listen(RegisterNotificationTypes::class, __CLASS__.'@registerNotificationType');
|
||||||
|
|
||||||
|
$events->listen(PostWasPosted::class, __CLASS__.'@whenPostWasPosted');
|
||||||
|
$events->listen(PostWasRevised::class, __CLASS__.'@whenPostWasRevised');
|
||||||
|
$events->listen(PostWasHidden::class, __CLASS__.'@whenPostWasHidden');
|
||||||
|
$events->listen(PostWasRestored::class, __CLASS__.'@whenPostWasRestored');
|
||||||
|
$events->listen(PostWasDeleted::class, __CLASS__.'@whenPostWasDeleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerNotificationType(RegisterNotificationTypes $event)
|
||||||
|
{
|
||||||
|
$event->register(
|
||||||
|
PostMentionedBlueprint::class,
|
||||||
|
'Flarum\Api\Serializers\PostBasicSerializer',
|
||||||
|
['alert']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function whenPostWasPosted(PostWasPosted $event)
|
||||||
|
{
|
||||||
|
$this->replyBecameVisible($event->post);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function whenPostWasRevised(PostWasRevised $event)
|
||||||
|
{
|
||||||
|
$this->replyBecameVisible($event->post);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function whenPostWasHidden(PostWasHidden $event)
|
||||||
|
{
|
||||||
|
$this->replyBecameInvisible($event->post);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function whenPostWasRestored(PostWasRestored $event)
|
||||||
|
{
|
||||||
|
$this->replyBecameVisible($event->post);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function whenPostWasDeleted(PostWasDeleted $event)
|
||||||
|
{
|
||||||
|
$this->replyBecameInvisible($event->post);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function replyBecameVisible(Post $reply)
|
||||||
|
{
|
||||||
|
$mentioned = Utils::getAttributeValues($reply->parsedContent, 'POSTMENTION', 'id');
|
||||||
|
|
||||||
|
$this->sync($reply, $mentioned);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function replyBecameInvisible(Post $reply)
|
||||||
|
{
|
||||||
|
$this->sync($reply, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sync(Post $reply, array $mentioned)
|
||||||
|
{
|
||||||
|
$reply->mentionsPosts()->sync($mentioned);
|
||||||
|
|
||||||
|
$posts = Post::with('user')
|
||||||
|
->whereIn('id', $mentioned)
|
||||||
|
->get()
|
||||||
|
->filter(function ($post) use ($reply) {
|
||||||
|
return $post->user->id !== $reply->user->id;
|
||||||
|
})
|
||||||
|
->all();
|
||||||
|
|
||||||
|
foreach ($posts as $post) {
|
||||||
|
$this->notifications->sync(new PostMentionedBlueprint($post, $reply), [$post->user]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
95
extensions/mentions/src/Listeners/UpdateUserMentionsMetadata.php
Executable file
95
extensions/mentions/src/Listeners/UpdateUserMentionsMetadata.php
Executable file
|
@ -0,0 +1,95 @@
|
||||||
|
<?php namespace Flarum\Mentions\Listeners;
|
||||||
|
|
||||||
|
use Flarum\Mentions\Notifications\UserMentionedBlueprint;
|
||||||
|
use Flarum\Core\Notifications\NotificationSyncer;
|
||||||
|
use Flarum\Events\RegisterNotificationTypes;
|
||||||
|
use Flarum\Events\PostWasPosted;
|
||||||
|
use Flarum\Events\PostWasRevised;
|
||||||
|
use Flarum\Events\PostWasHidden;
|
||||||
|
use Flarum\Events\PostWasRestored;
|
||||||
|
use Flarum\Events\PostWasDeleted;
|
||||||
|
use Flarum\Core\Posts\Post;
|
||||||
|
use Flarum\Core\Users\User;
|
||||||
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
|
use s9e\TextFormatter\Utils;
|
||||||
|
|
||||||
|
class UpdateUserMentionsMetadata
|
||||||
|
{
|
||||||
|
protected $notifications;
|
||||||
|
|
||||||
|
public function __construct(NotificationSyncer $notifications)
|
||||||
|
{
|
||||||
|
$this->notifications = $notifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function subscribe(Dispatcher $events)
|
||||||
|
{
|
||||||
|
$events->listen(RegisterNotificationTypes::class, __CLASS__.'@registerNotificationType');
|
||||||
|
|
||||||
|
$events->listen(PostWasPosted::class, __CLASS__.'@whenPostWasPosted');
|
||||||
|
$events->listen(PostWasRevised::class, __CLASS__.'@whenPostWasRevised');
|
||||||
|
$events->listen(PostWasHidden::class, __CLASS__.'@whenPostWasHidden');
|
||||||
|
$events->listen(PostWasRestored::class, __CLASS__.'@whenPostWasRestored');
|
||||||
|
$events->listen(PostWasDeleted::class, __CLASS__.'@whenPostWasDeleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerNotificationType(RegisterNotificationTypes $event)
|
||||||
|
{
|
||||||
|
$event->register(
|
||||||
|
UserMentionedBlueprint::class,
|
||||||
|
'Flarum\Api\Serializers\PostBasicSerializer',
|
||||||
|
['alert']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function whenPostWasPosted(PostWasPosted $event)
|
||||||
|
{
|
||||||
|
$this->postBecameVisible($event->post);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function whenPostWasRevised(PostWasRevised $event)
|
||||||
|
{
|
||||||
|
$this->postBecameVisible($event->post);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function whenPostWasHidden(PostWasHidden $event)
|
||||||
|
{
|
||||||
|
$this->postBecameInvisible($event->post);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function whenPostWasRestored(PostWasRestored $event)
|
||||||
|
{
|
||||||
|
$this->postBecameVisible($event->post);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function whenPostWasDeleted(PostWasDeleted $event)
|
||||||
|
{
|
||||||
|
$this->postBecameInvisible($event->post);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function postBecameVisible(Post $post)
|
||||||
|
{
|
||||||
|
$mentioned = Utils::getAttributeValues($post->parsedContent, 'USERMENTION', 'id');
|
||||||
|
|
||||||
|
$this->sync($post, $mentioned);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function postBecameInvisible(Post $post)
|
||||||
|
{
|
||||||
|
$this->sync($post, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sync(Post $post, array $mentioned)
|
||||||
|
{
|
||||||
|
$post->mentionsUsers()->sync($mentioned);
|
||||||
|
|
||||||
|
$users = User::whereIn('id', $mentioned)
|
||||||
|
->get()
|
||||||
|
->filter(function ($user) use ($post) {
|
||||||
|
return $user->id !== $post->user->id;
|
||||||
|
})
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$this->notifications->sync(new UserMentionedBlueprint($post), $users);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
<?php namespace Flarum\Mentions;
|
|
||||||
|
|
||||||
abstract class MentionsParserAbstract
|
|
||||||
{
|
|
||||||
protected $pattern;
|
|
||||||
|
|
||||||
public function match($string)
|
|
||||||
{
|
|
||||||
preg_match_all($this->pattern, $string, $matches);
|
|
||||||
|
|
||||||
return $matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function replace($string, $callback)
|
|
||||||
{
|
|
||||||
return preg_replace_callback($this->pattern, function ($matches) use ($callback) {
|
|
||||||
return $callback($matches);
|
|
||||||
}, $string);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
<?php namespace Flarum\Mentions;
|
|
||||||
|
|
||||||
use Flarum\Support\ServiceProvider;
|
|
||||||
use Flarum\Extend;
|
|
||||||
|
|
||||||
class MentionsServiceProvider extends ServiceProvider
|
|
||||||
{
|
|
||||||
public function boot()
|
|
||||||
{
|
|
||||||
$this->loadViewsFrom(__DIR__.'/../views', 'mentions');
|
|
||||||
|
|
||||||
$this->extend(
|
|
||||||
new Extend\EventSubscriber([
|
|
||||||
'Flarum\Mentions\Handlers\PostMentionsMetadataUpdater',
|
|
||||||
'Flarum\Mentions\Handlers\UserMentionsMetadataUpdater'
|
|
||||||
]),
|
|
||||||
|
|
||||||
(new Extend\ForumClient())
|
|
||||||
->assets([
|
|
||||||
__DIR__.'/../js/dist/extension.js',
|
|
||||||
__DIR__.'/../less/mentions.less'
|
|
||||||
]),
|
|
||||||
|
|
||||||
(new Extend\Model('Flarum\Core\Models\Post'))
|
|
||||||
->belongsToMany('mentionedBy', 'Flarum\Core\Models\Post', 'mentions_posts', 'mentions_id')
|
|
||||||
->belongsToMany('mentionsPosts', 'Flarum\Core\Models\Post', 'mentions_posts', 'post_id', 'mentions_id')
|
|
||||||
->belongsToMany('mentionsUsers', 'Flarum\Core\Models\User', 'mentions_users', 'post_id', 'mentions_id'),
|
|
||||||
|
|
||||||
(new Extend\ApiSerializer('Flarum\Api\Serializers\PostSerializer'))
|
|
||||||
->hasMany('mentionedBy', 'Flarum\Api\Serializers\PostBasicSerializer')
|
|
||||||
->hasMany('mentionsPosts', 'Flarum\Api\Serializers\PostBasicSerializer')
|
|
||||||
->hasMany('mentionsUsers', 'Flarum\Api\Serializers\UserBasicSerializer'),
|
|
||||||
|
|
||||||
(new Extend\ApiAction('Flarum\Api\Actions\Discussions\ShowAction'))
|
|
||||||
->addInclude('posts.mentionedBy')
|
|
||||||
->addInclude('posts.mentionedBy.user')
|
|
||||||
->addLink('posts.mentionedBy.discussion')
|
|
||||||
->addInclude('posts.mentionsPosts', false)
|
|
||||||
->addInclude('posts.mentionsPosts.user', false)
|
|
||||||
->addInclude('posts.mentionsUsers', false),
|
|
||||||
|
|
||||||
(new Extend\ApiAction([
|
|
||||||
'Flarum\Api\Actions\Posts\IndexAction',
|
|
||||||
'Flarum\Api\Actions\Posts\ShowAction',
|
|
||||||
]))
|
|
||||||
->addInclude('mentionedBy')
|
|
||||||
->addInclude('mentionedBy.user')
|
|
||||||
->addLink('mentionedBy.discussion'),
|
|
||||||
|
|
||||||
(new Extend\ApiAction('Flarum\Api\Actions\Posts\CreateAction'))
|
|
||||||
->addInclude('mentionsPosts')
|
|
||||||
->addInclude('mentionsPosts.mentionedBy'),
|
|
||||||
|
|
||||||
new Extend\Formatter('postMentions', 'Flarum\Mentions\PostMentionsFormatter'),
|
|
||||||
|
|
||||||
new Extend\Formatter('userMentions', 'Flarum\Mentions\UserMentionsFormatter'),
|
|
||||||
|
|
||||||
new Extend\ActivityType('Flarum\Mentions\PostMentionedActivity', 'Flarum\Api\Serializers\PostBasicSerializer'),
|
|
||||||
|
|
||||||
new Extend\ActivityType('Flarum\Mentions\UserMentionedActivity', 'Flarum\Api\Serializers\PostBasicSerializer'),
|
|
||||||
|
|
||||||
(new Extend\NotificationType('Flarum\Mentions\PostMentionedNotification', 'Flarum\Api\Serializers\PostBasicSerializer'))
|
|
||||||
->enableByDefault('alert'),
|
|
||||||
|
|
||||||
(new Extend\NotificationType('Flarum\Mentions\UserMentionedNotification', 'Flarum\Api\Serializers\PostBasicSerializer'))
|
|
||||||
->enableByDefault('alert')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +1,10 @@
|
||||||
<?php namespace Flarum\Mentions;
|
<?php namespace Flarum\Mentions\Notifications;
|
||||||
|
|
||||||
use Flarum\Core\Models\Post;
|
use Flarum\Core\Posts\Post;
|
||||||
use Flarum\Core\Notifications\NotificationAbstract;
|
use Flarum\Core\Notifications\Blueprint;
|
||||||
|
use Flarum\Core\Notifications\MailableBlueprint;
|
||||||
|
|
||||||
class PostMentionedNotification extends NotificationAbstract
|
class PostMentionedBlueprint implements Blueprint, MailableBlueprint
|
||||||
{
|
{
|
||||||
public $post;
|
public $post;
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ class PostMentionedNotification extends NotificationAbstract
|
||||||
|
|
||||||
public function getData()
|
public function getData()
|
||||||
{
|
{
|
||||||
return ['replyNumber' => $this->reply->number];
|
return ['replyNumber' => (int) $this->reply->number];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getEmailView()
|
public function getEmailView()
|
||||||
|
@ -47,11 +48,6 @@ class PostMentionedNotification extends NotificationAbstract
|
||||||
|
|
||||||
public static function getSubjectModel()
|
public static function getSubjectModel()
|
||||||
{
|
{
|
||||||
return 'Flarum\Core\Models\Post';
|
return Post::class;
|
||||||
}
|
|
||||||
|
|
||||||
public static function isEmailable()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,10 +1,11 @@
|
||||||
<?php namespace Flarum\Mentions;
|
<?php namespace Flarum\Mentions\Notifications;
|
||||||
|
|
||||||
use Flarum\Core\Models\User;
|
use Flarum\Core\Users\User;
|
||||||
use Flarum\Core\Models\Post;
|
use Flarum\Core\Posts\Post;
|
||||||
use Flarum\Core\Notifications\NotificationAbstract;
|
use Flarum\Core\Notifications\Blueprint;
|
||||||
|
use Flarum\Core\Notifications\MailableBlueprint;
|
||||||
|
|
||||||
class UserMentionedNotification extends NotificationAbstract
|
class UserMentionedBlueprint implements Blueprint, MailableBlueprint
|
||||||
{
|
{
|
||||||
public $post;
|
public $post;
|
||||||
|
|
||||||
|
@ -23,6 +24,11 @@ class UserMentionedNotification extends NotificationAbstract
|
||||||
return $this->post->user;
|
return $this->post->user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getData()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public function getEmailView()
|
public function getEmailView()
|
||||||
{
|
{
|
||||||
return ['text' => 'mentions::emails.userMentioned'];
|
return ['text' => 'mentions::emails.userMentioned'];
|
||||||
|
@ -40,11 +46,6 @@ class UserMentionedNotification extends NotificationAbstract
|
||||||
|
|
||||||
public static function getSubjectModel()
|
public static function getSubjectModel()
|
||||||
{
|
{
|
||||||
return 'Flarum\Core\Models\Post';
|
return Post::class;
|
||||||
}
|
|
||||||
|
|
||||||
public static function isEmailable()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,37 +0,0 @@
|
||||||
<?php namespace Flarum\Mentions;
|
|
||||||
|
|
||||||
use Flarum\Core\Models\Post;
|
|
||||||
use Flarum\Core\Activity\ActivityAbstract;
|
|
||||||
|
|
||||||
class PostMentionedActivity extends ActivityAbstract
|
|
||||||
{
|
|
||||||
public $post;
|
|
||||||
|
|
||||||
public $reply;
|
|
||||||
|
|
||||||
public function __construct(Post $post, Post $reply)
|
|
||||||
{
|
|
||||||
$this->post = $post;
|
|
||||||
$this->reply = $reply;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSubject()
|
|
||||||
{
|
|
||||||
return $this->reply;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTime()
|
|
||||||
{
|
|
||||||
return $this->reply->time;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getType()
|
|
||||||
{
|
|
||||||
return 'postMentioned';
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getSubjectModel()
|
|
||||||
{
|
|
||||||
return 'Flarum\Core\Models\Post';
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
<?php namespace Flarum\Mentions;
|
|
||||||
|
|
||||||
use Flarum\Core\Formatter\FormatterAbstract;
|
|
||||||
use Flarum\Core\Models\Post;
|
|
||||||
|
|
||||||
class PostMentionsFormatter extends FormatterAbstract
|
|
||||||
{
|
|
||||||
protected $parser;
|
|
||||||
|
|
||||||
public function __construct(PostMentionsParser $parser)
|
|
||||||
{
|
|
||||||
$this->parser = $parser;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function afterPurification($text, Post $post = null)
|
|
||||||
{
|
|
||||||
if ($post) {
|
|
||||||
$text = $this->ignoreTags($text, ['a', 'code', 'pre'], function ($text) use ($post) {
|
|
||||||
return $this->parser->replace($text, function ($match) use ($post) {
|
|
||||||
// TODO: use URL generator
|
|
||||||
return '<a href="/d/'.$post->discussion_id.'/-/'.$match['number'].'" class="mention-post" data-number="'.$match['number'].'">'.$match['username'].'</a>';
|
|
||||||
}, $text);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return $text;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?php namespace Flarum\Mentions;
|
|
||||||
|
|
||||||
class PostMentionsParser extends MentionsParserAbstract
|
|
||||||
{
|
|
||||||
protected $pattern = '/\B@(?P<username>[a-z0-9_-]+)#(?P<number>\d+)/i';
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
<?php namespace Flarum\Mentions;
|
|
||||||
|
|
||||||
use Flarum\Core\Models\Post;
|
|
||||||
use Flarum\Core\Activity\ActivityAbstract;
|
|
||||||
|
|
||||||
class UserMentionedActivity extends ActivityAbstract
|
|
||||||
{
|
|
||||||
public $post;
|
|
||||||
|
|
||||||
public function __construct(Post $post)
|
|
||||||
{
|
|
||||||
$this->post = $post;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSubject()
|
|
||||||
{
|
|
||||||
return $this->post;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTime()
|
|
||||||
{
|
|
||||||
return $this->post->time;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getType()
|
|
||||||
{
|
|
||||||
return 'userMentioned';
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getSubjectModel()
|
|
||||||
{
|
|
||||||
return 'Flarum\Core\Models\Post';
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
<?php namespace Flarum\Mentions;
|
|
||||||
|
|
||||||
use Flarum\Core\Formatter\FormatterAbstract;
|
|
||||||
use Flarum\Core\Models\Post;
|
|
||||||
|
|
||||||
class UserMentionsFormatter extends FormatterAbstract
|
|
||||||
{
|
|
||||||
protected $parser;
|
|
||||||
|
|
||||||
public function __construct(UserMentionsParser $parser)
|
|
||||||
{
|
|
||||||
$this->parser = $parser;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function afterPurification($text, Post $post = null)
|
|
||||||
{
|
|
||||||
$text = $this->ignoreTags($text, ['a', 'code', 'pre'], function ($text) {
|
|
||||||
return $this->parser->replace($text, function ($match) {
|
|
||||||
// TODO: use URL generator
|
|
||||||
return '<a href="/u/'.$match['username'].'" class="mention-user" data-user="'.$match['username'].'">'.$match['username'].'</a>';
|
|
||||||
}, $text);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $text;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?php namespace Flarum\Mentions;
|
|
||||||
|
|
||||||
class UserMentionsParser extends MentionsParserAbstract
|
|
||||||
{
|
|
||||||
protected $pattern = '/\B@(?P<username>[a-z0-9_-]+)(?!#)/i';
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user