Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wip] [mentions] feat: tag mentions #3688

Closed
wants to merge 37 commits into from
Closed
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
847db82
wip: begin creating tag mention feature
imorland Nov 21, 2022
b8914f7
Apply fixes from StyleCI
StyleCIBot Nov 21, 2022
fb29374
chore: format js
imorland Nov 21, 2022
31fdba3
implement relation sync
imorland Nov 21, 2022
c7895b6
Apply fixes from StyleCI
StyleCIBot Nov 21, 2022
f1483ab
use the new conditional migration
imorland Nov 21, 2022
bd07106
Adjust format of tag mentions
imorland Nov 21, 2022
7ab9085
implement permission
imorland Nov 21, 2022
bf86a45
Apply fixes from StyleCI
StyleCIBot Nov 21, 2022
83558f0
autocomplete working, but need to refactor out to use # for tagmentions
imorland Nov 21, 2022
574b281
chore: deprecate getMentionText, replace with MentionTextGenerator
imorland Nov 21, 2022
c70eedd
typos
imorland Nov 21, 2022
cce76da
avoid code duplication
imorland Nov 21, 2022
a28e7ea
add missing return
imorland Nov 21, 2022
290c744
compat
imorland Nov 21, 2022
0777d4f
Merge branch 'main' into im/mention-tags
imorland Dec 5, 2022
3cf6908
chore: move backend logic to tags ext
imorland Dec 5, 2022
da751a7
Apply fixes from StyleCI
StyleCIBot Dec 5, 2022
06ea4c2
Merge branch 'main' into im/mention-tags
imorland Dec 5, 2022
cbc6839
Apply fixes from StyleCI
StyleCIBot Dec 5, 2022
e48faac
typo in class name
imorland Dec 5, 2022
ad9b2f8
move listener to tags ext
imorland Dec 5, 2022
132ca73
Apply fixes from StyleCI
StyleCIBot Dec 5, 2022
158f06c
Move remaining backend tagMention relations to tags ext
imorland Dec 5, 2022
2e01cf9
cleanup extend
imorland Dec 5, 2022
070ed9d
Move permission to tags ext
imorland Dec 5, 2022
6ef7a07
move permission pt2
imorland Dec 5, 2022
5b218e9
revert now unneeded userattributes class
imorland Dec 5, 2022
ff23293
move formatter for tag mentions to tags
imorland Dec 5, 2022
625eaed
check for mentions enabled, not tags
imorland Dec 5, 2022
ccc25ba
Extract tagmention data for syncing
imorland Dec 5, 2022
a90a2d2
remove 2nd param
imorland Dec 5, 2022
7016f6d
move import
imorland Dec 5, 2022
4ab9ba9
Merge branch 'main' into im/mention-tags
imorland Dec 13, 2022
eb11255
move tag mention generator
imorland Dec 13, 2022
8bbc4f9
composer autocomplete, remove permission
imorland Dec 13, 2022
15ea184
remove mentions enabled check
imorland Dec 13, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion extensions/mentions/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@
"name": "fas fa-at",
"backgroundColor": "#539EC1",
"color": "#fff"
}
},
"optional-dependencies": [
"flarum/tags"
]
},
"flarum-cli": {
"modules": {
Expand Down
27 changes: 15 additions & 12 deletions extensions/mentions/extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
use Flarum\Post\Event\Revised;
use Flarum\Post\Filter\PostFilterer;
use Flarum\Post\Post;
use Flarum\Tags\Api\Serializer\TagSerializer;
use Flarum\Tags\Tag;
use Flarum\User\User;

return [
Expand All @@ -39,6 +41,7 @@
->render(Formatter\FormatPostMentions::class)
->render(Formatter\FormatUserMentions::class)
->render(Formatter\FormatGroupMentions::class)
->render(Formatter\FormatTagMentions::class)
->unparse(Formatter\UnparsePostMentions::class)
->unparse(Formatter\UnparseUserMentions::class)
->parse(Formatter\CheckPermissions::class),
Expand All @@ -47,7 +50,8 @@
->belongsToMany('mentionedBy', Post::class, 'post_mentions_post', 'mentions_post_id', 'post_id')
->belongsToMany('mentionsPosts', Post::class, 'post_mentions_post', 'post_id', 'mentions_post_id')
->belongsToMany('mentionsUsers', User::class, 'post_mentions_user', 'post_id', 'mentions_user_id')
->belongsToMany('mentionsGroups', Group::class, 'post_mentions_group', 'post_id', 'mentions_group_id'),
->belongsToMany('mentionsGroups', Group::class, 'post_mentions_group', 'post_id', 'mentions_group_id')
->belongsToMany('mentionsTags', Tag::class, 'post_mentions_tag', 'post_id', 'mentions_tag_id'),

new Extend\Locales(__DIR__.'/locale'),

Expand All @@ -63,20 +67,21 @@
->hasMany('mentionedBy', BasicPostSerializer::class)
->hasMany('mentionsPosts', BasicPostSerializer::class)
->hasMany('mentionsUsers', BasicUserSerializer::class)
->hasMany('mentionsGroups', GroupSerializer::class),
->hasMany('mentionsGroups', GroupSerializer::class)
->hasMany('mentionsTags', TagSerializer::class),

(new Extend\ApiController(Controller\ShowDiscussionController::class))
->addInclude(['posts.mentionedBy', 'posts.mentionedBy.user', 'posts.mentionedBy.discussion'])
->addInclude(['posts.mentionedBy', 'posts.mentionedBy.user', 'posts.mentionedBy.discussion', 'posts.mentionsTags'])
->load([
'posts.mentionsUsers', 'posts.mentionsPosts', 'posts.mentionsPosts.user', 'posts.mentionedBy',
'posts.mentionedBy.mentionsPosts', 'posts.mentionedBy.mentionsPosts.user', 'posts.mentionedBy.mentionsUsers',
'posts.mentionsGroups'
'posts.mentionsGroups', 'posts.mentionsTags',
]),

(new Extend\ApiController(Controller\ListDiscussionsController::class))
->load([
'firstPost.mentionsUsers', 'firstPost.mentionsPosts', 'firstPost.mentionsPosts.user', 'firstPost.mentionsGroups',
'lastPost.mentionsUsers', 'lastPost.mentionsPosts', 'lastPost.mentionsPosts.user', 'lastPost.mentionsGroups'
'firstPost.mentionsUsers', 'firstPost.mentionsPosts', 'firstPost.mentionsPosts.user', 'firstPost.mentionsGroups', 'firstPost.mentionsTags',
'lastPost.mentionsUsers', 'lastPost.mentionsPosts', 'lastPost.mentionsPosts.user', 'lastPost.mentionsGroups', 'lastPost.mentionsTags',
]),

(new Extend\ApiController(Controller\ShowPostController::class))
Expand All @@ -87,16 +92,16 @@
->load([
'mentionsUsers', 'mentionsPosts', 'mentionsPosts.user', 'mentionedBy',
'mentionedBy.mentionsPosts', 'mentionedBy.mentionsPosts.user', 'mentionedBy.mentionsUsers',
'mentionsGroups'
'mentionsGroups', 'mentionsTags',
]),

(new Extend\ApiController(Controller\CreatePostController::class))
->addInclude(['mentionsPosts', 'mentionsPosts.mentionedBy'])
->addOptionalInclude('mentionsGroups'),
->addOptionalInclude(['mentionsGroups', 'mentionsTags']),

(new Extend\ApiController(Controller\UpdatePostController::class))
->addInclude(['mentionsPosts', 'mentionsPosts.mentionedBy'])
->addOptionalInclude('mentionsGroups'),
->addOptionalInclude(['mentionsGroups', 'mentionsTags']),

(new Extend\ApiController(Controller\AbstractSerializeController::class))
->prepareDataForSerialization(FilterVisiblePosts::class),
Expand All @@ -115,7 +120,5 @@
->addFilter(Filter\MentionedFilter::class),

(new Extend\ApiSerializer(CurrentUserSerializer::class))
->attribute('canMentionGroups', function (CurrentUserSerializer $serializer, User $user, array $attributes): bool {
return $user->can('mentionGroups');
})
->attributes(AddCurrentUserAttributes::class),
];
13 changes: 12 additions & 1 deletion extensions/mentions/js/src/admin/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import app from 'flarum/admin/app';

app.initializers.add('flarum-mentions', function () {
app.extensionData
const extData = app.extensionData
.for('flarum-mentions')
.registerSetting({
setting: 'flarum-mentions.allow_username_format',
Expand All @@ -17,4 +17,15 @@ app.initializers.add('flarum-mentions', function () {
},
'start'
);

if (app.initializers.has('flarum-tags')) {
extData.registerPermission(
{
permission: 'mentionTags',
label: app.translator.trans('flarum-mentions.admin.permissions.mention_tags_label'),
icon: 'fas fa-at',
},
'start'
);
}
});
58 changes: 53 additions & 5 deletions extensions/mentions/js/src/forum/addComposerAutocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import Badge from 'flarum/common/components/Badge';
import Group from 'flarum/common/models/Group';

import AutocompleteDropdown from './fragments/AutocompleteDropdown';
import getMentionText from './utils/getMentionText';
import MentionTextGenerator from './utils/MentionTextGenerator';

const tagsEnabled = !!app.initializers.has('flarum-tags');

const throttledSearch = throttle(
250, // 250ms timeout
Expand Down Expand Up @@ -63,6 +65,8 @@ export default function addComposerAutocomplete() {
let typed;
let matchTyped;

const mentionTextGenerator = new MentionTextGenerator();

// We store users returned from an API here to preserve order in which they are returned
// This prevents the user list jumping around while users are returned.
// We also use a hashset for user IDs to provide O(1) lookup for the users already in the list.
Expand All @@ -76,6 +80,12 @@ export default function addComposerAutocomplete() {
})
);

let returnedTags;
if (tagsEnabled) {
// Store tags..
returnedTags = Array.from(app.store.all('tags'));
}

const applySuggestion = (replacement) => {
this.attrs.composer.editor.replaceBeforeCursor(absMentionStart - 1, replacement + ' ');

Expand Down Expand Up @@ -157,6 +167,29 @@ export default function addComposerAutocomplete() {
);
};

const makeTagSuggestion = function (tag, replacement, content, className = '') {
let tagName = tag.name().toLowerCase();

if (typed) {
tagName = highlight(tagName, typed);
}

return (
<button
className={'PostPreview ' + className}
onclick={() => applySuggestion(replacement)}
onmouseenter={function () {
dropdown.setIndex($(this).parent().index());
}}
>
<span className="PostPreview-content">
<Badge class={`Avatar Badge Badge--tag--${tag.id()} Badge-icon `} color={tag.color()} type="tag" icon={tag.icon()} />
<span className="username">{tagName}</span>
</span>
</button>
);
};

const userMatches = function (user) {
const names = [user.username(), user.displayName()];

Expand All @@ -169,6 +202,12 @@ export default function addComposerAutocomplete() {
return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed);
};

const tagMatches = function (tag) {
const names = [tag.name()];

return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed);
};

const buildSuggestions = () => {
const suggestions = [];

Expand All @@ -178,15 +217,24 @@ export default function addComposerAutocomplete() {
returnedUsers.forEach((user) => {
if (!userMatches(user)) return;

suggestions.push(makeSuggestion(user, getMentionText(user), '', 'MentionsDropdown-user'));
suggestions.push(makeSuggestion(user, mentionTextGenerator.forUser(user), '', 'MentionsDropdown-user'));
});

// ... or groups.
if (app.session?.user?.canMentionGroups()) {
returnedGroups.forEach((group) => {
if (!groupMatches(group)) return;

suggestions.push(makeGroupSuggestion(group, getMentionText(undefined, undefined, group), '', 'MentionsDropdown-group'));
suggestions.push(makeGroupSuggestion(group, mentionTextGenerator.forGroup(group), '', 'MentionsDropdown-group'));
});
}

// ... or tags.
if (tagsEnabled && app.session?.user?.canMentionTags?.()) {
returnedTags.forEach((tag) => {
if (!tagMatches(tag)) return;

suggestions.push(makeTagSuggestion(tag, mentionTextGenerator.forTag(tag), '', 'MentionsDropdown-tag'));
});
}
}
Expand Down Expand Up @@ -220,7 +268,7 @@ export default function addComposerAutocomplete() {
suggestions.push(
makeSuggestion(
user,
getMentionText(user, post.id()),
mentionTextGenerator.forPostMention(user, post.id()),
[
app.translator.trans('flarum-mentions.forum.composer.reply_to_post_text', { number: post.number() }),
' — ',
Expand Down Expand Up @@ -271,7 +319,7 @@ export default function addComposerAutocomplete() {
dropdown.setIndex(0);
dropdown.$().scrollTop(0);

// Don't send API calls searching for users until at least 2 characters have been typed.
// Don't send API calls searching for users or tags until at least 2 characters have been typed.
// This focuses the mention results on users and posts in the discussion.
if (typed.length > 1 && app.forum.attribute('canSearchUsers')) {
throttledSearch(typed, searched, returnedUsers, returnedUserIds, dropdown, buildSuggestions);
Expand Down
2 changes: 2 additions & 0 deletions extensions/mentions/js/src/forum/compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import AutocompleteDropdown from './fragments/AutocompleteDropdown';
import PostQuoteButton from './fragments/PostQuoteButton';
import getCleanDisplayName from './utils/getCleanDisplayName';
import getMentionText from './utils/getMentionText';
import MentionTextGenerator from './utils/MentionTextGenerator';
import * as reply from './utils/reply';
import selectedText from './utils/selectedText';
import * as textFormatter from './utils/textFormatter';
Expand All @@ -19,6 +20,7 @@ export default {
'mentions/fragments/PostQuoteButton': PostQuoteButton,
'mentions/utils/getCleanDisplayName': getCleanDisplayName,
'mentions/utils/getMentionText': getMentionText,
'mentions/utils/MentionTextGenerator': MentionTextGenerator,
'mentions/utils/reply': reply,
'mentions/utils/selectedText': selectedText,
'mentions/utils/textFormatter': textFormatter,
Expand Down
4 changes: 4 additions & 0 deletions extensions/mentions/js/src/forum/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ import Model from 'flarum/common/Model';
app.initializers.add('flarum-mentions', function () {
User.prototype.canMentionGroups = Model.attribute('canMentionGroups');

if (app.initializers.has('flarum-tags')) {
User.prototype.canMentionTags = Model.attribute('canMentionTags');
}

// 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();
Expand Down
83 changes: 83 additions & 0 deletions extensions/mentions/js/src/forum/utils/MentionTextGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import getCleanDisplayName, { shouldUseOldFormat } from './getCleanDisplayName';
import type User from 'flarum/common/models/User';
import type Group from 'flarum/common/models/Group';
import type Tag from 'flarum/tags/common/models/Tag';

/**
* Fetches the mention text for a specified model.
*/
export default class MentionTextGenerator {
/**
* Automatically determines which mention syntax to be used based on the option in the
* admin dashboard. Also performs display name clean-up automatically.
*
* @"Display name"#UserID or `@username`
*
* @example <caption>New display name syntax</caption>
* // '@"user"#1'
* forUser(User) // User is ID 1, display name is 'User'
*
* @example <caption>Using old syntax</caption>
* // '@username'
* forUser(user) // User's username is 'username'
*
* @param user
* @returns string
*/
forUser(user: User): string {
if (shouldUseOldFormat()) {
const cleanText = getCleanDisplayName(user, false);
return `@${cleanText}`;
}
const cleanText = getCleanDisplayName(user);
return `@"${cleanText}"#${user.id()}`;
}

/**
* Generates the syntax for mentioning of a post. Also cleans up the display name.
*
* @example <caption>Post mention</caption>
* // '@"User"#p13'
* // @"Display name"#pPostID
* forPostMention(user, 13) // User display name is 'User', post ID is 13
*
* @param user
* @param postId
* @returns
*/
forPostMention(user: User, postId: number): string {
const cleanText = getCleanDisplayName(user);
return `@"${cleanText}"#p${postId}`;
}

/**
* Generates the mention syntax for a group mention.
*
* @"Name Plural"#gGroupID
*
* @example <caption>Group mention</caption>
* // '@"Mods"#g4'
* forGroup(group) // Group display name is 'Mods', group ID is 4
*
* @param group
* @returns string
*/
forGroup(group: Group): string {
return `@"${group.namePlural()}"#g${group.id()}`;
}

/**
* Generates the mention syntax for a tag mention.
*
* @example <caption>Tag mention</caption>
* // '@"General"#t1'
* // @"Name"#tTagID
* forTag(tag) // Tag display name is 'General', tag ID is 1
*
* @param tag
* @returns
*/
forTag(tag: Tag): string {
return `@"${tag.name()}"#t${tag.id()}`;
}
}
Loading