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

feat: search UI/UX revamp #3941

Merged
merged 19 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 13 additions & 29 deletions extensions/emoji/js/src/forum/addComposerAutocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { extend } from 'flarum/common/extend';
import TextEditorButton from 'flarum/common/components/TextEditorButton';
import KeyboardNavigatable from 'flarum/common/utils/KeyboardNavigatable';
import Tooltip from 'flarum/common/components/Tooltip';
import AutocompleteReader from 'flarum/common/utils/AutocompleteReader';

import AutocompleteDropdown from './fragments/AutocompleteDropdown';
import getEmojiIconCode from './helpers/getEmojiIconCode';
Expand Down Expand Up @@ -40,15 +41,7 @@ export default function addComposerAutocomplete() {
extend('flarum/common/components/TextEditor', 'buildEditorParams', function (params) {
const emojiKeys = Object.keys(emojiMap);

let relEmojiStart;
let absEmojiStart;
let typed;

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

this.emojiDropdown.hide();
};
const autocompleteReader = new AutocompleteReader(':');

params.inputListeners.push(() => {
const selection = this.attrs.composer.editor.getSelectionRange();
Expand All @@ -57,29 +50,20 @@ export default function addComposerAutocomplete() {

if (selection[1] - cursor > 0) return;

// Search backwards from the cursor for an ':' symbol. If we find
// one and followed by a whitespace, we will want to show the
// autocomplete dropdown!
const lastChunk = this.attrs.composer.editor.getLastNChars(15);
absEmojiStart = 0;
for (let i = lastChunk.length - 1; i >= 0; i--) {
const character = lastChunk.substr(i, 1);
// check what user typed, emoji names only contains alphanumeric,
// underline, '+' and '-'
if (!/[a-z0-9]|\+|\-|_|\:/.test(character)) break;
// make sure ':' preceded by a whitespace or newline
if (character === ':' && (i == 0 || /\s/.test(lastChunk.substr(i - 1, 1)))) {
relEmojiStart = i + 1;
absEmojiStart = cursor - lastChunk.length + i + 1;
break;
}
}
const autocompleting = autocompleteReader.check(lastChunk, cursor, /[a-z0-9]|\+|\-|_|\:/);

this.emojiDropdown.hide();
this.emojiDropdown.active = false;

if (absEmojiStart) {
typed = lastChunk.substring(relEmojiStart).toLowerCase();
if (autocompleting) {
const typed = autocompleting.typed;
const emojiDropdown = this.emojiDropdown;

const applySuggestion = (replacement) => {
this.attrs.composer.editor.replaceBeforeCursor(autocompleting.absoluteStart - 1, replacement + ' ');
this.emojiDropdown.hide();
};

const makeSuggestion = function ({ emoji, name, code }) {
return (
Expand All @@ -88,7 +72,7 @@ export default function addComposerAutocomplete() {
key={emoji}
onclick={() => applySuggestion(emoji)}
onmouseenter={function () {
this.emojiDropdown.setIndex($(this).parent().index() - 1);
emojiDropdown.setIndex($(this).parent().index() - 1);
}}
>
<img alt={emoji} className="emoji" draggable="false" loading="lazy" src={`${cdn}72x72/${code}.png`} title={name} />
Expand Down Expand Up @@ -152,7 +136,7 @@ export default function addComposerAutocomplete() {
m.render(this.$('.ComposerBody-emojiDropdownContainer')[0], this.emojiDropdown.render());

this.emojiDropdown.show();
const coordinates = this.attrs.composer.editor.getCaretCoordinates(absEmojiStart);
const coordinates = this.attrs.composer.editor.getCaretCoordinates(autocompleting.absoluteStart);
const width = this.emojiDropdown.$().outerWidth();
const height = this.emojiDropdown.$().outerHeight();
const parent = this.emojiDropdown.$().offsetParent();
Expand Down
21 changes: 5 additions & 16 deletions extensions/lock/js/src/common/query/discussions/LockedGambit.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import IGambit from 'flarum/common/query/IGambit';
import { BooleanGambit } from 'flarum/common/query/IGambit';
import app from 'flarum/common/app';

export default class LockedGambit implements IGambit {
pattern(): string {
return 'is:locked';
}

toFilter(_matches: string[], negate: boolean): Record<string, any> {
const key = (negate ? '-' : '') + 'locked';

return {
[key]: true,
};
export default class LockedGambit extends BooleanGambit {
key(): string {
return app.translator.trans('flarum-lock.lib.gambits.discussions.locked.key', {}, true);
}

filterKey(): string {
return 'locked';
}

fromFilter(value: string, negate: boolean): string {
return `${negate ? '-' : ''}is:locked`;
}
}
9 changes: 9 additions & 0 deletions extensions/lock/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,12 @@ flarum-lock:
# These translations are used in the Settings page.
settings:
notify_discussion_locked_label: Someone locks a discussion I started

# Translations in this namespace are used by the forum and admin interfaces.
lib:

# These translations are used by gambits. Gambit keys must be in snake_case, no spaces.
gambits:
discussions:
locked:
key: locked
55 changes: 21 additions & 34 deletions extensions/mentions/js/src/forum/addComposerAutocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import app from 'flarum/forum/app';
import { extend } from 'flarum/common/extend';
import TextEditorButton from 'flarum/common/components/TextEditorButton';
import KeyboardNavigatable from 'flarum/common/utils/KeyboardNavigatable';
import AutocompleteReader from 'flarum/common/utils/AutocompleteReader';
import { throttle } from 'flarum/common/utils/throttleDebounce';

import AutocompleteDropdown from './fragments/AutocompleteDropdown';
import MentionableModels from './mentionables/MentionableModels';

export default function addComposerAutocomplete() {
extend('flarum/common/components/TextEditor', 'onbuild', function () {
this.mentionsDropdown = new AutocompleteDropdown();
this.searchMentions = throttle(250, (mentionables, buildSuggestions) => mentionables.search().then(buildSuggestions));
const $editor = this.$('.TextEditor-editor').wrap('<div class="ComposerBody-mentionsWrapper"></div>');

this.navigator = new KeyboardNavigatable();
Expand All @@ -24,52 +27,36 @@ export default function addComposerAutocomplete() {
});

extend('flarum/common/components/TextEditor', 'buildEditorParams', function (params) {
let relMentionStart;
let absMentionStart;
let matchTyped;

let mentionables = new MentionableModels({
onmouseenter: function () {
this.mentionsDropdown.setIndex($(this).parent().index());
},
onclick: (replacement) => {
this.attrs.composer.editor.replaceBeforeCursor(absMentionStart - 1, replacement + ' ');

this.mentionsDropdown.hide();
},
});

const suggestionsInputListener = () => {
const selection = this.attrs.composer.editor.getSelectionRange();

const cursor = selection[0];

if (selection[1] - cursor > 0) return;

// Search backwards from the cursor for a mention triggering symbol. If we find one,
// we will want to show the correct autocomplete dropdown!
// Check classes implementing the IMentionableModel interface to see triggering symbols.
const lastChunk = this.attrs.composer.editor.getLastNChars(30);
absMentionStart = 0;
let activeFormat = null;
for (let i = lastChunk.length - 1; i >= 0; i--) {
const character = lastChunk.substr(i, 1);
activeFormat = app.mentionFormats.get(character);

if (activeFormat && (i === 0 || /\s/.test(lastChunk.substr(i - 1, 1)))) {
relMentionStart = i + 1;
absMentionStart = cursor - lastChunk.length + i + 1;
mentionables.init(activeFormat.makeMentionables());
break;
}
}
const autocompleteReader = new AutocompleteReader((character) => !!(activeFormat = app.mentionFormats.get(character)));
const autocompleting = autocompleteReader.check(this.attrs.composer.editor.getLastNChars(30), cursor, /\S+/);

const mentionsDropdown = this.mentionsDropdown;
let mentionables = new MentionableModels({
onmouseenter: function () {
mentionsDropdown.setIndex($(this).parent().index());
},
onclick: (replacement) => {
this.attrs.composer.editor.replaceBeforeCursor(autocompleting.absoluteStart - 1, replacement + ' ');
this.mentionsDropdown.hide();
},
});

this.mentionsDropdown.hide();
this.mentionsDropdown.active = false;

if (absMentionStart) {
const typed = lastChunk.substring(relMentionStart).toLowerCase();
matchTyped = activeFormat.queryFromTyped(typed);
if (autocompleting) {
mentionables.init(activeFormat.makeMentionables());
matchTyped = activeFormat.queryFromTyped(autocompleting.typed);

if (!matchTyped) return;

Expand All @@ -85,7 +72,7 @@ export default function addComposerAutocomplete() {
m.render(this.$('.ComposerBody-mentionsDropdownContainer')[0], this.mentionsDropdown.render());

this.mentionsDropdown.show();
const coordinates = this.attrs.composer.editor.getCaretCoordinates(absMentionStart);
const coordinates = this.attrs.composer.editor.getCaretCoordinates(autocompleting.absoluteStart);
const width = this.mentionsDropdown.$().outerWidth();
const height = this.mentionsDropdown.$().outerHeight();
const parent = this.mentionsDropdown.$().offsetParent();
Expand Down Expand Up @@ -118,7 +105,7 @@ export default function addComposerAutocomplete() {
this.mentionsDropdown.setIndex(0);
this.mentionsDropdown.$().scrollTop(0);

mentionables.search()?.then(buildSuggestions);
this.searchMentions(mentionables, buildSuggestions);
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type MentionableModel from './MentionableModel';
import type Model from 'flarum/common/Model';
import type Mithril from 'mithril';
import MentionsDropdownItem from '../components/MentionsDropdownItem';
import { throttle } from 'flarum/common/utils/throttleDebounce';

export default class MentionableModels {
protected mentionables?: MentionableModel[];
Expand Down Expand Up @@ -33,7 +32,7 @@ export default class MentionableModels {
* Don't send API calls searching for models until at least 2 characters have been typed.
* This focuses the mention results on models already loaded.
*/
public readonly search = throttle(250, async (): Promise<void> => {
public readonly search = async (): Promise<void> => {
if (!this.typed || this.typed.length <= 1) return;

const typedLower = this.typed.toLowerCase();
Expand All @@ -51,7 +50,7 @@ export default class MentionableModels {
this.searched.push(typedLower);

return Promise.resolve();
});
};

public matches(mentionable: MentionableModel, model: Model): boolean {
return mentionable.matches(model, this.typed?.toLowerCase() || '');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import IGambit from 'flarum/common/query/IGambit';
import { BooleanGambit } from 'flarum/common/query/IGambit';
import app from 'flarum/common/app';

export default class StickyGambit implements IGambit {
pattern(): string {
return 'is:sticky';
}

toFilter(_matches: string[], negate: boolean): Record<string, any> {
const key = (negate ? '-' : '') + 'sticky';

return {
[key]: true,
};
export default class StickyGambit extends BooleanGambit {
key(): string {
return app.translator.trans('flarum-sticky.lib.gambits.discussions.sticky.key', {}, true);
}

filterKey(): string {
return 'sticky';
}

fromFilter(value: string, negate: boolean): string {
return `${negate ? '-' : ''}is:sticky`;
}
}
9 changes: 9 additions & 0 deletions extensions/sticky/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ flarum-sticky:
# REUSED TRANSLATIONS - These keys should not be used directly in code!
##

# Translations in this namespace are used by the forum and admin interfaces.
lib:

# These translations are used by gambits. Gambit keys must be in snake_case, no spaces.
gambits:
discussions:
sticky:
key: sticky

# Translations in this namespace are referenced by two or more unique keys.
ref:
sticky: Sticky
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import IGambit from 'flarum/common/query/IGambit';
import app from 'flarum/common/app';
import { BooleanGambit } from 'flarum/common/query/IGambit';

export default class SubscriptionGambit implements IGambit {
pattern(): string {
return 'is:(follow|ignor)(?:ing|ed)';
export default class SubscriptionGambit extends BooleanGambit {
key(): string[] {
return [
app.translator.trans('flarum-subscriptions.lib.gambits.discussions.subscription.following_key', {}, true),
app.translator.trans('flarum-subscriptions.lib.gambits.discussions.subscription.ignoring_key', {}, true),
];
}

toFilter(matches: string[], negate: boolean): Record<string, any> {
const type = matches[1] === 'follow' ? 'following' : 'ignoring';
const key = (negate ? '-' : '') + this.filterKey();

return {
subscription: type,
[key]: matches[1],
};
}

Expand All @@ -20,4 +24,8 @@ export default class SubscriptionGambit implements IGambit {
fromFilter(value: string, negate: boolean): string {
return `${negate ? '-' : ''}is:${value}`;
}

enabled(): boolean {
return !!app.session.user;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import GlobalSearchState from 'flarum/forum/states/GlobalSearchState';
export default function addSubscriptionFilter() {
extend(IndexSidebar.prototype, 'navItems', function (items) {
if (app.session.user) {
const params = app.search.stickyParams();
const params = app.search.state.stickyParams();

items.add(
'following',
Expand Down
10 changes: 10 additions & 0 deletions extensions/subscriptions/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ flarum-subscriptions:
# REUSED TRANSLATIONS - These keys should not be used directly in code!
##

# Translations in this namespace are used by the forum and admin interfaces.
lib:

# These translations are used by gambits. Gambit keys must be in snake_case, no spaces.
gambits:
discussions:
subscription:
following_key: following
ignoring_key: ignoring

# Translations in this namespace are referenced by two or more unique keys.
ref:
follow: Follow
Expand Down
6 changes: 6 additions & 0 deletions extensions/suspend/extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

use Flarum\Api\Serializer\BasicUserSerializer;
use Flarum\Api\Serializer\ForumSerializer;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Extend;
use Flarum\Search\Database\DatabaseSearchDriver;
Expand Down Expand Up @@ -41,6 +42,11 @@
(new Extend\ApiSerializer(UserSerializer::class))
->attributes(AddUserSuspendAttributes::class),

(new Extend\ApiSerializer(ForumSerializer::class))
->attribute('canSuspendUsers', function (ForumSerializer $serializer) {
return $serializer->getActor()->hasPermission('user.suspend');
}),

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

(new Extend\Notification())
Expand Down
7 changes: 7 additions & 0 deletions extensions/suspend/js/src/@types/shims.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'flarum/common/models/User';

declare module 'flarum/common/models/User' {
export default interface User {
canSuspend: () => boolean;
}
}
Loading
Loading