mirror of
https://github.com/flarum/framework.git
synced 2024-11-23 15:34:10 +08:00
parent
bc4356a7f5
commit
b74ecbfacf
2
.github/workflows/backend.yml
vendored
2
.github/workflows/backend.yml
vendored
|
@ -8,4 +8,4 @@ jobs:
|
|||
with:
|
||||
enable_backend_testing: true
|
||||
backend_directory: .
|
||||
monorepo_tests: "framework/core extensions/akismet extensions/approval extensions/flags extensions/likes extensions/mentions extensions/nicknames extensions/statistics extensions/sticky extensions/subscriptions extensions/suspend extensions/tags"
|
||||
monorepo_tests: "framework/core extensions/akismet extensions/approval extensions/flags extensions/likes extensions/mentions extensions/nicknames extensions/statistics extensions/sticky extensions/subscriptions extensions/suspend extensions/tags extensions/messages"
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
"Flarum\\Subscriptions\\": "extensions/subscriptions/src",
|
||||
"Flarum\\Suspend\\": "extensions/suspend/src",
|
||||
"Flarum\\Tags\\": "extensions/tags/src",
|
||||
"Flarum\\Messages\\": "extensions/messages/src",
|
||||
"Flarum\\PHPStan\\": "php-packages/phpstan/src",
|
||||
"Flarum\\Testing\\": "php-packages/testing/src"
|
||||
},
|
||||
|
@ -77,6 +78,7 @@
|
|||
"Flarum\\Subscriptions\\Tests\\": "extensions/subscriptions/tests",
|
||||
"Flarum\\Suspend\\Tests\\": "extensions/suspend/tests",
|
||||
"Flarum\\Tags\\Tests\\": "extensions/tags/tests",
|
||||
"Flarum\\Messages\\Tests\\": "extensions/messages/tests",
|
||||
"Flarum\\Testing\\Tests\\": "php-packages/testing/tests"
|
||||
}
|
||||
},
|
||||
|
@ -101,6 +103,7 @@
|
|||
"flarum/subscriptions": "self.version",
|
||||
"flarum/suspend": "self.version",
|
||||
"flarum/tags": "self.version",
|
||||
"flarum/messages": "self.version",
|
||||
"flarum/phpstan": "self.version",
|
||||
"flarum/testing": "self.version"
|
||||
},
|
||||
|
@ -196,7 +199,8 @@
|
|||
"extensions/sticky",
|
||||
"extensions/subscriptions",
|
||||
"extensions/suspend",
|
||||
"extensions/tags"
|
||||
"extensions/tags",
|
||||
"extensions/messages"
|
||||
],
|
||||
"branch-alias": {
|
||||
"dev-main": "2.x-dev"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2021 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2019-2024 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2021 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2019-2024 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2021 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2019-2024 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2021 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2019-2024 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2021 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2019-2024 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2021 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2019-2024 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
|
|
@ -10,6 +10,7 @@ import type Post from 'flarum/common/models/Post';
|
|||
import type FlagListState from '../states/FlagListState';
|
||||
import type Flag from '../models/Flag';
|
||||
import { Page } from 'flarum/common/states/PaginatedListState';
|
||||
import ItemList from 'flarum/common/utils/ItemList';
|
||||
|
||||
export interface IFlagListAttrs extends ComponentAttrs {
|
||||
state: FlagListState;
|
||||
|
@ -27,6 +28,7 @@ export default class FlagList<CustomAttrs extends IFlagListAttrs = IFlagListAttr
|
|||
<HeaderList
|
||||
className="FlagList"
|
||||
title={app.translator.trans('flarum-flags.forum.flagged_posts.title')}
|
||||
controls={this.controlItems()}
|
||||
hasItems={state.hasItems()}
|
||||
loading={state.isLoading()}
|
||||
emptyText={app.translator.trans('flarum-flags.forum.flagged_posts.empty_text')}
|
||||
|
@ -37,6 +39,12 @@ export default class FlagList<CustomAttrs extends IFlagListAttrs = IFlagListAttr
|
|||
);
|
||||
}
|
||||
|
||||
controlItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
content(state: FlagListState) {
|
||||
if (!state.isLoading() && state.hasItems()) {
|
||||
return state.getPages().map((page: Page<Flag>) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2021 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2019-2024 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2021 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2019-2024 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
|
|
@ -14,7 +14,7 @@ use Flarum\Api\Schema;
|
|||
use Flarum\Likes\Event\PostWasLiked;
|
||||
use Flarum\Likes\Event\PostWasUnliked;
|
||||
use Flarum\Post\Post;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Query\Expression;
|
||||
|
||||
class PostResourceFields
|
||||
|
@ -51,7 +51,7 @@ class PostResourceFields
|
|||
Schema\Relationship\ToMany::make('likes')
|
||||
->type('users')
|
||||
->includable()
|
||||
->scope(function (Builder $query, Context $context) {
|
||||
->scope(function (BelongsToMany $query, Context $context) {
|
||||
$actor = $context->getActor();
|
||||
$grammar = $query->getQuery()->getGrammar();
|
||||
|
||||
|
|
|
@ -10,11 +10,12 @@
|
|||
namespace Flarum\Likes\Notification;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Notification\AlertableInterface;
|
||||
use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
|
||||
class PostLikedBlueprint implements BlueprintInterface
|
||||
class PostLikedBlueprint implements BlueprintInterface, AlertableInterface
|
||||
{
|
||||
public function __construct(
|
||||
public Post $post,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2021 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2019-2024 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
|
|
@ -12,10 +12,11 @@ namespace Flarum\Lock\Notification;
|
|||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Lock\Post\DiscussionLockedPost;
|
||||
use Flarum\Notification\AlertableInterface;
|
||||
use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||
use Flarum\User\User;
|
||||
|
||||
class DiscussionLockedBlueprint implements BlueprintInterface
|
||||
class DiscussionLockedBlueprint implements BlueprintInterface, AlertableInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected DiscussionLockedPost $post
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2021 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2019-2024 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
||||
Copyright (c) 2017-2018 GitHub, Inc.
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2021 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2019-2024 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
|
|
@ -9,8 +9,10 @@
|
|||
|
||||
namespace Flarum\Mentions\Api;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Schema;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class PostResourceFields
|
||||
{
|
||||
|
@ -20,12 +22,14 @@ class PostResourceFields
|
|||
{
|
||||
return [
|
||||
Schema\Integer::make('mentionedByCount')
|
||||
->countRelation('mentionedBy'),
|
||||
->countRelation('mentionedBy', function (Builder $query, Context $context) {
|
||||
$query->whereVisibleTo($context->getActor());
|
||||
}),
|
||||
|
||||
Schema\Relationship\ToMany::make('mentionedBy')
|
||||
->type('posts')
|
||||
->includable()
|
||||
->scope(fn (Builder $query) => $query->oldest('id')->limit(static::$maxMentionedBy)),
|
||||
->scope(fn (BelongsToMany $query) => $query->oldest('id')->limit(static::$maxMentionedBy)),
|
||||
Schema\Relationship\ToMany::make('mentionsPosts')
|
||||
->type('posts'),
|
||||
Schema\Relationship\ToMany::make('mentionsUsers')
|
||||
|
|
|
@ -11,12 +11,13 @@ namespace Flarum\Mentions\Notification;
|
|||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Notification\AlertableInterface;
|
||||
use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||
use Flarum\Notification\MailableInterface;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
|
||||
class GroupMentionedBlueprint implements BlueprintInterface, MailableInterface
|
||||
class GroupMentionedBlueprint implements BlueprintInterface, AlertableInterface, MailableInterface
|
||||
{
|
||||
public function __construct(
|
||||
public Post $post
|
||||
|
|
|
@ -11,12 +11,13 @@ namespace Flarum\Mentions\Notification;
|
|||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Notification\AlertableInterface;
|
||||
use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||
use Flarum\Notification\MailableInterface;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
|
||||
class PostMentionedBlueprint implements BlueprintInterface, MailableInterface
|
||||
class PostMentionedBlueprint implements BlueprintInterface, AlertableInterface, MailableInterface
|
||||
{
|
||||
public function __construct(
|
||||
public Post $post,
|
||||
|
|
|
@ -11,12 +11,13 @@ namespace Flarum\Mentions\Notification;
|
|||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Notification\AlertableInterface;
|
||||
use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||
use Flarum\Notification\MailableInterface;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
|
||||
class UserMentionedBlueprint implements BlueprintInterface, MailableInterface
|
||||
class UserMentionedBlueprint implements BlueprintInterface, AlertableInterface, MailableInterface
|
||||
{
|
||||
public function __construct(
|
||||
public Post $post
|
||||
|
|
19
extensions/messages/.editorconfig
Normal file
19
extensions/messages/.editorconfig
Normal file
|
@ -0,0 +1,19 @@
|
|||
# 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
|
||||
|
||||
[*.{diff,md}]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{php,xml,json}]
|
||||
indent_size = 4
|
20
extensions/messages/.gitattributes
vendored
Normal file
20
extensions/messages/.gitattributes
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
**/.gitattributes export-ignore
|
||||
**/.gitignore export-ignore
|
||||
**/.gitmodules export-ignore
|
||||
**/.github export-ignore
|
||||
**/.travis export-ignore
|
||||
**/.travis.yml export-ignore
|
||||
**/.editorconfig export-ignore
|
||||
**/.styleci.yml export-ignore
|
||||
|
||||
**/phpunit.xml export-ignore
|
||||
**/tests export-ignore
|
||||
|
||||
**/js/dist/**/* -diff
|
||||
**/js/dist/**/* linguist-generated
|
||||
**/js/dist-typings/**/* -diff
|
||||
**/js/dist-typings/**/* linguist-generated
|
||||
**/js/yarn.lock -diff
|
||||
**/js/package-lock.json -diff
|
||||
|
||||
* text=auto eol=lf
|
13
extensions/messages/.gitignore
vendored
Normal file
13
extensions/messages/.gitignore
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
/vendor
|
||||
composer.lock
|
||||
composer.phar
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
tests/.phpunit.cache
|
||||
tests/.phpunit.result.cache
|
||||
/tests/integration/tmp
|
||||
.vagrant
|
||||
.idea/*
|
||||
.vscode
|
||||
js/coverage-ts
|
22
extensions/messages/LICENSE
Normal file
22
extensions/messages/LICENSE
Normal file
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2024 Stichting Flarum (Flarum Foundation)
|
||||
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
80
extensions/messages/composer.json
Normal file
80
extensions/messages/composer.json
Normal file
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
"name": "flarum/messages",
|
||||
"description": "Private messaging ",
|
||||
"keywords": [
|
||||
"flarum"
|
||||
],
|
||||
"type": "flarum-extension",
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"flarum/core": "^2.0"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Flarum",
|
||||
"email": "info@flarum.org",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Flarum\\Messages\\": "src/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"flarum-extension": {
|
||||
"title": "Messages",
|
||||
"category": "feature",
|
||||
"icon": {
|
||||
"name": "fas fa-envelope-open",
|
||||
"color": "#ffffff",
|
||||
"backgroundColor": "#9b34c7"
|
||||
}
|
||||
},
|
||||
"flarum-cli": {
|
||||
"modules": {
|
||||
"admin": true,
|
||||
"forum": true,
|
||||
"js": true,
|
||||
"jsCommon": true,
|
||||
"css": true,
|
||||
"locale": true,
|
||||
"gitConf": true,
|
||||
"githubActions": true,
|
||||
"prettier": true,
|
||||
"typescript": true,
|
||||
"bundlewatch": false,
|
||||
"frontendTesting": true,
|
||||
"backendTesting": true,
|
||||
"phpstan": false,
|
||||
"editorConfig": true,
|
||||
"styleci": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Flarum\\Messages\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": [
|
||||
"@test:unit",
|
||||
"@test:integration"
|
||||
],
|
||||
"test:unit": "phpunit -c tests/phpunit.unit.xml",
|
||||
"test:integration": "phpunit -c tests/phpunit.integration.xml",
|
||||
"test:setup": "@php tests/integration/setup.php"
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
"test": "Runs all tests.",
|
||||
"test:unit": "Runs all unit tests.",
|
||||
"test:integration": "Runs all integration tests.",
|
||||
"test:setup": "Sets up a database for use with integration tests. Execute this only once."
|
||||
},
|
||||
"require-dev": {
|
||||
"flarum/testing": "^2.0"
|
||||
}
|
||||
}
|
87
extensions/messages/extend.php
Normal file
87
extensions/messages/extend.php
Normal file
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages;
|
||||
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Messages\Http\Middleware\PopulateDialogWithActor;
|
||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
return [
|
||||
(new Extend\Frontend('forum'))
|
||||
->js(__DIR__.'/js/dist/forum.js')
|
||||
->css(__DIR__.'/less/forum.less')
|
||||
->jsDirectory(__DIR__.'/js/dist/forum')
|
||||
->route('/messages', 'messages')
|
||||
->route('/messages/dialog/{id:\d+}', 'messages.dialog'),
|
||||
|
||||
(new Extend\Frontend('admin'))
|
||||
->js(__DIR__.'/js/dist/admin.js')
|
||||
->css(__DIR__.'/less/admin.less'),
|
||||
|
||||
new Extend\Locales(__DIR__.'/locale'),
|
||||
|
||||
(new Extend\View())->namespace('flarum-messages', __DIR__.'/views'),
|
||||
|
||||
(new Extend\Model(User::class))
|
||||
->belongsToMany('dialogs', Dialog::class, 'dialog_user')
|
||||
->hasMany('dialogMessages', DialogMessage::class, 'user_id'),
|
||||
|
||||
(new Extend\ModelVisibility(Dialog::class))
|
||||
->scope(Access\ScopeDialogVisibility::class),
|
||||
|
||||
(new Extend\ModelVisibility(DialogMessage::class))
|
||||
->scope(Access\ScopeDialogMessageVisibility::class),
|
||||
|
||||
new Extend\ApiResource(Api\Resource\DialogResource::class),
|
||||
|
||||
new Extend\ApiResource(Api\Resource\DialogMessageResource::class),
|
||||
|
||||
(new Extend\ApiResource(Resource\UserResource::class))
|
||||
->fields(fn () => [
|
||||
Schema\Boolean::make('canSendAnyMessage')
|
||||
->get(fn (object $model, Context $context) => $context->getActor()->can('sendAnyMessage')),
|
||||
Schema\Integer::make('messageCount')
|
||||
->get(function (object $model, Context $context) {
|
||||
return Dialog::whereVisibleTo($context->getActor())
|
||||
->whereHas('users', function (Builder $query) use ($context) {
|
||||
$query->where('dialog_user.user_id', $context->getActor()->id)
|
||||
->whereColumn('dialog_user.last_read_message_id', '<', 'dialogs.last_message_id');
|
||||
})->count();
|
||||
}),
|
||||
]),
|
||||
|
||||
(new Extend\Middleware('api'))
|
||||
->add(PopulateDialogWithActor::class),
|
||||
|
||||
(new Extend\Policy())
|
||||
->modelPolicy(Dialog::class, Access\DialogPolicy::class)
|
||||
->modelPolicy(DialogMessage::class, Access\DialogMessagePolicy::class)
|
||||
->globalPolicy(Access\GlobalPolicy::class),
|
||||
|
||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||
->addSearcher(Dialog::class, Search\DialogSearcher::class)
|
||||
->addSearcher(DialogMessage::class, Search\DialogMessageSearcher::class)
|
||||
->addFilter(Search\DialogMessageSearcher::class, DialogMessage\Filter\DialogFilter::class)
|
||||
->addFilter(Search\DialogSearcher::class, Dialog\Filter\UnreadFilter::class),
|
||||
|
||||
(new Extend\ServiceProvider())
|
||||
->register(DialogServiceProvider::class),
|
||||
|
||||
(new Extend\Notification())
|
||||
->type(Notification\MessageReceivedBlueprint::class, ['email']),
|
||||
|
||||
(new Extend\Event())
|
||||
->listen(DialogMessage\Event\Created::class, Listener\SendNotificationWhenMessageSent::class),
|
||||
];
|
9
extensions/messages/js/.gitignore
vendored
Normal file
9
extensions/messages/js/.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
node_modules
|
2
extensions/messages/js/admin.ts
Normal file
2
extensions/messages/js/admin.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './src/common';
|
||||
export * from './src/admin';
|
2
extensions/messages/js/forum.ts
Normal file
2
extensions/messages/js/forum.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './src/common';
|
||||
export * from './src/forum';
|
1
extensions/messages/js/jest.config.cjs
Normal file
1
extensions/messages/js/jest.config.cjs
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = require('@flarum/jest-config')({});
|
31
extensions/messages/js/package.json
Normal file
31
extensions/messages/js/package.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "@flarum/messages",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"flarum-webpack-config": "^3.0.0",
|
||||
"webpack": "^5.65.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"prettier": "^2.5.1",
|
||||
"@flarum/prettier-config": "^1.0.0",
|
||||
"flarum-tsconfig": "^1.0.2",
|
||||
"typescript": "^4.5.4",
|
||||
"typescript-coverage-report": "^0.6.1",
|
||||
"@flarum/jest-config": "^1.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "webpack --mode development --watch",
|
||||
"build": "webpack --mode production",
|
||||
"analyze": "cross-env ANALYZER=true yarn run build",
|
||||
"format": "prettier --write src",
|
||||
"format-check": "prettier --check src",
|
||||
"clean-typings": "npx rimraf dist-typings && mkdir dist-typings",
|
||||
"build-typings": "yarn run clean-typings && ([ -e src/@types ] && cp -r src/@types dist-typings/@types || true) && tsc && yarn run post-build-typings",
|
||||
"post-build-typings": "find dist-typings -type f -name '*.d.ts' -print0 | xargs -0 sed -i 's,../src/@types,@types,g'",
|
||||
"check-typings": "tsc --noEmit --emitDeclarationOnly false",
|
||||
"check-typings-coverage": "typescript-coverage-report",
|
||||
"test": "yarn node --experimental-vm-modules $(yarn bin jest)"
|
||||
},
|
||||
"prettier": "@flarum/prettier-config",
|
||||
"type": "module"
|
||||
}
|
21
extensions/messages/js/src/@types/shims.d.ts
vendored
Normal file
21
extensions/messages/js/src/@types/shims.d.ts
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
import type Dialog from '../common/models/Dialog';
|
||||
import DialogListState from '../forum/states/DialogListState';
|
||||
|
||||
declare module 'flarum/forum/routes' {
|
||||
export interface ForumRoutes {
|
||||
dialog: (tag: Dialog) => string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'flarum/forum/ForumApplication' {
|
||||
export default interface ForumApplication {
|
||||
dialogs: DialogListState;
|
||||
dropdownDialogs: DialogListState;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'flarum/forum/states/ComposerState' {
|
||||
export default interface ComposerState {
|
||||
composingMessageTo(dialog: Dialog): boolean;
|
||||
}
|
||||
}
|
8
extensions/messages/js/src/admin/extend.ts
Normal file
8
extensions/messages/js/src/admin/extend.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import Extend from 'flarum/common/extenders';
|
||||
import commonExtend from '../common/extend';
|
||||
|
||||
export default [
|
||||
...commonExtend,
|
||||
|
||||
// Add your admin extenders here
|
||||
];
|
16
extensions/messages/js/src/admin/index.ts
Normal file
16
extensions/messages/js/src/admin/index.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import app from 'flarum/admin/app';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-messages', () => {
|
||||
app.extensionData.for('flarum-messages').registerPermission(
|
||||
{
|
||||
icon: 'fas fa-envelope-open-text',
|
||||
label: app.translator.trans('flarum-messages.admin.permissions.send_messages'),
|
||||
permission: 'dialog.sendMessage',
|
||||
allowGuest: false,
|
||||
},
|
||||
'start',
|
||||
98
|
||||
);
|
||||
});
|
9
extensions/messages/js/src/common/extend.ts
Normal file
9
extensions/messages/js/src/common/extend.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import DialogMessage from './models/DialogMessage';
|
||||
import Dialog from './models/Dialog';
|
||||
import Extend from 'flarum/common/extenders';
|
||||
|
||||
export default [
|
||||
new Extend.Store()
|
||||
.add('dialogs', Dialog) //
|
||||
.add('dialog-messages', DialogMessage), //
|
||||
];
|
1
extensions/messages/js/src/common/index.ts
Normal file
1
extensions/messages/js/src/common/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export default null;
|
45
extensions/messages/js/src/common/models/Dialog.ts
Normal file
45
extensions/messages/js/src/common/models/Dialog.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import Model from 'flarum/common/Model';
|
||||
import User from 'flarum/common/models/User';
|
||||
import DialogMessage from './DialogMessage';
|
||||
import app from 'flarum/common/app';
|
||||
|
||||
export default class Dialog extends Model {
|
||||
title() {
|
||||
return Model.attribute<string>('title').call(this);
|
||||
}
|
||||
type() {
|
||||
return Model.attribute<string>('type').call(this);
|
||||
}
|
||||
lastMessageAt() {
|
||||
return Model.attribute<Date, string>('lastMessageAt', Model.transformDate).call(this);
|
||||
}
|
||||
createdAt() {
|
||||
return Model.attribute<Date, string>('createdAt', Model.transformDate).call(this);
|
||||
}
|
||||
|
||||
users() {
|
||||
return Model.hasMany<User>('users').call(this);
|
||||
}
|
||||
firstMessage() {
|
||||
return Model.hasOne<DialogMessage>('firstMessage').call(this);
|
||||
}
|
||||
lastMessage() {
|
||||
return Model.hasOne<DialogMessage>('lastMessage').call(this);
|
||||
}
|
||||
|
||||
unreadCount() {
|
||||
return Model.attribute<number>('unreadCount').call(this);
|
||||
}
|
||||
lastReadMessageId() {
|
||||
return Model.attribute<number>('lastReadMessageId').call(this);
|
||||
}
|
||||
lastReadAt() {
|
||||
return Model.attribute<Date, string>('lastReadAt', Model.transformDate).call(this);
|
||||
}
|
||||
|
||||
recipient() {
|
||||
let users = this.users();
|
||||
|
||||
return !users ? null : users.find((user) => user && user.id() !== app.session.user!.id());
|
||||
}
|
||||
}
|
36
extensions/messages/js/src/common/models/DialogMessage.ts
Normal file
36
extensions/messages/js/src/common/models/DialogMessage.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import Model from 'flarum/common/Model';
|
||||
import computed from 'flarum/common/utils/computed';
|
||||
import { getPlainContent } from 'flarum/common/utils/string';
|
||||
import type Dialog from './Dialog';
|
||||
import type User from 'flarum/common/models/User';
|
||||
|
||||
export default class DialogMessage extends Model {
|
||||
content() {
|
||||
return Model.attribute<string | null | undefined>('content').call(this);
|
||||
}
|
||||
contentHtml() {
|
||||
return Model.attribute<string | null | undefined>('contentHtml').call(this);
|
||||
}
|
||||
renderFailed() {
|
||||
return Model.attribute<boolean | undefined>('renderFailed').call(this);
|
||||
}
|
||||
contentPlain() {
|
||||
return computed<string | null | undefined>('contentHtml', (content) => {
|
||||
if (typeof content === 'string') {
|
||||
return getPlainContent(content);
|
||||
}
|
||||
|
||||
return content as null | undefined;
|
||||
}).call(this);
|
||||
}
|
||||
createdAt() {
|
||||
return Model.attribute<Date, string>('createdAt', Model.transformDate).call(this);
|
||||
}
|
||||
|
||||
dialog() {
|
||||
return Model.hasOne<Dialog>('dialog').call(this);
|
||||
}
|
||||
user() {
|
||||
return Model.hasOne<User>('user').call(this);
|
||||
}
|
||||
}
|
65
extensions/messages/js/src/forum/components/DetailsModal.tsx
Normal file
65
extensions/messages/js/src/forum/components/DetailsModal.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import app from 'flarum/forum/app';
|
||||
import Modal, { type IInternalModalAttrs } from 'flarum/common/components/Modal';
|
||||
import type Dialog from '../../common/models/Dialog';
|
||||
import type User from 'flarum/common/models/User';
|
||||
import ItemList from 'flarum/common/utils/ItemList';
|
||||
import Mithril from 'mithril';
|
||||
import Avatar from 'flarum/common/components/Avatar';
|
||||
import fullTime from 'flarum/common/helpers/fullTime';
|
||||
import username from 'flarum/common/helpers/username';
|
||||
import Link from 'flarum/common/components/Link';
|
||||
import listItems from 'flarum/common/helpers/listItems';
|
||||
|
||||
export interface IDetailsModalAttrs extends IInternalModalAttrs {
|
||||
dialog: Dialog;
|
||||
}
|
||||
|
||||
export default class DetailsModal<CustomAttrs extends IDetailsModalAttrs = IDetailsModalAttrs> extends Modal<CustomAttrs> {
|
||||
className() {
|
||||
return 'Modal--small Modal--flat DetailsModal';
|
||||
}
|
||||
|
||||
title() {
|
||||
return app.translator.trans('flarum-messages.forum.dialog_section.details_modal.title');
|
||||
}
|
||||
|
||||
content() {
|
||||
let recipients = (this.attrs.dialog.users() || []).filter(Boolean) as User[];
|
||||
|
||||
return (
|
||||
<div className="Modal-body DetailsModal-infoGroups">
|
||||
<div className="DetailsModal-recipients DetailsModal-info">
|
||||
<div className="DetailsModal-info-title">{app.translator.trans('flarum-messages.forum.dialog_section.details_modal.recipients')}</div>
|
||||
<div className="DetailsModal-recipients-list">
|
||||
{recipients?.map((recipient: User) => {
|
||||
return (
|
||||
<div className="DetailsModal-recipient">
|
||||
<Avatar user={recipient} />
|
||||
<Link href={app.route('user', { username: recipient.slug() })}>
|
||||
<span className="DetailsModal-recipient-username">{username(recipient)}</span>
|
||||
</Link>
|
||||
<div className="badges">{listItems(recipient.badges().toArray())}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{this.infoItems().toArray()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
infoItems() {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
|
||||
items.add(
|
||||
'created',
|
||||
<div className="DetailsModal-createdAt DetailsModal-info">
|
||||
<div className="DetailsModal-info-title">{app.translator.trans('flarum-messages.forum.dialog_section.details_modal.created_at')}</div>
|
||||
<div className="DetailsModal-info-content">{fullTime(this.attrs.dialog.createdAt())}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import app from 'flarum/forum/app';
|
||||
import Component from 'flarum/common/Component';
|
||||
import type { ComponentAttrs } from 'flarum/common/Component';
|
||||
import HeaderList from 'flarum/forum/components/HeaderList';
|
||||
import type Mithril from 'mithril';
|
||||
import DialogListState from '../states/DialogListState';
|
||||
import DialogList from './DialogList';
|
||||
import LinkButton from 'flarum/common/components/LinkButton';
|
||||
import ItemList from 'flarum/common/utils/ItemList';
|
||||
import Tooltip from 'flarum/common/components/Tooltip';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
|
||||
export interface IDialogListDropdownAttrs extends ComponentAttrs {
|
||||
state: DialogListState;
|
||||
}
|
||||
|
||||
export default class DialogDropdownList<CustomAttrs extends IDialogListDropdownAttrs = IDialogListDropdownAttrs> extends Component<
|
||||
CustomAttrs,
|
||||
DialogListState
|
||||
> {
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
}
|
||||
|
||||
view() {
|
||||
const state = this.attrs.state;
|
||||
|
||||
return (
|
||||
<HeaderList
|
||||
className="DialogDropdownList"
|
||||
title={app.translator.trans('flarum-messages.forum.dialog_list.title')}
|
||||
controls={this.controlItems()}
|
||||
hasItems={state.hasItems()}
|
||||
loading={state.isLoading()}
|
||||
emptyText={app.translator.trans('flarum-messages.forum.messages_page.empty_text')}
|
||||
loadMore={() => state.hasNext() && !state.isLoadingNext() && state.loadNext()}
|
||||
footer={() => (
|
||||
<h4>
|
||||
<LinkButton href={app.route('messages')} className="Button Button--link" icon="fas fa-inbox">
|
||||
{app.translator.trans('flarum-messages.forum.dialog_list.view_all')}
|
||||
</LinkButton>
|
||||
</h4>
|
||||
)}
|
||||
>
|
||||
<div className="HeaderListGroup-content">{this.content()}</div>
|
||||
</HeaderList>
|
||||
);
|
||||
}
|
||||
|
||||
controlItems() {
|
||||
const items = new ItemList();
|
||||
const state = this.attrs.state;
|
||||
|
||||
if (app.session.user!.attribute<number>('messageCount') > 0) {
|
||||
items.add(
|
||||
'mark_all_as_read',
|
||||
<Tooltip text={app.translator.trans('flarum-messages.forum.messages_page.mark_all_as_read_tooltip')}>
|
||||
<Button
|
||||
className="Button Button--link"
|
||||
data-container=".DialogDropdownList"
|
||||
icon="fas fa-check"
|
||||
title={app.translator.trans('flarum-messages.forum.messages_page.mark_all_as_read_tooltip')}
|
||||
onclick={state.markAllAsRead.bind(state)}
|
||||
/>
|
||||
</Tooltip>,
|
||||
70
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
content() {
|
||||
return <DialogList state={this.attrs.state} hideMore={true} itemActions={true} />;
|
||||
}
|
||||
}
|
47
extensions/messages/js/src/forum/components/DialogList.tsx
Normal file
47
extensions/messages/js/src/forum/components/DialogList.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import app from 'flarum/forum/app';
|
||||
import Component, { type ComponentAttrs } from 'flarum/common/Component';
|
||||
import type Mithril from 'mithril';
|
||||
import DialogListState from '../states/DialogListState';
|
||||
import Dialog from '../../common/models/Dialog';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import DialogListItem from './DialogListItem';
|
||||
|
||||
export interface IDialogListAttrs extends ComponentAttrs {
|
||||
state: DialogListState;
|
||||
activeDialog?: Dialog | null;
|
||||
hideMore?: boolean;
|
||||
itemActions?: boolean;
|
||||
}
|
||||
|
||||
export default class DialogList<CustomAttrs extends IDialogListAttrs = IDialogListAttrs> extends Component<CustomAttrs> {
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
}
|
||||
|
||||
oncreate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||
super.oncreate(vnode);
|
||||
}
|
||||
|
||||
onupdate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||
super.onupdate(vnode);
|
||||
}
|
||||
|
||||
view() {
|
||||
return (
|
||||
<div className="DialogList">
|
||||
<ul className="DialogList-list">
|
||||
{this.attrs.state.getAllItems().map((dialog) => (
|
||||
<DialogListItem dialog={dialog} active={this.attrs.activeDialog?.id() === dialog.id()} actions={this.attrs.itemActions} />
|
||||
))}
|
||||
</ul>
|
||||
{this.attrs.state.hasNext() && !this.attrs.hideMore && (
|
||||
<div className="DialogList-loadMore">
|
||||
<Button className="Button" onclick={this.attrs.state.loadNext.bind(this.attrs.state)}>
|
||||
{app.translator.trans('flarum-messages.forum.dialog_list.load_more_button')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
import Component, { type ComponentAttrs } from 'flarum/common/Component';
|
||||
import Mithril from 'mithril';
|
||||
import classList from 'flarum/common/utils/classList';
|
||||
import Link from 'flarum/common/components/Link';
|
||||
import app from 'flarum/forum/app';
|
||||
import Avatar from 'flarum/common/components/Avatar';
|
||||
import username from 'flarum/common/helpers/username';
|
||||
import humanTime from 'flarum/common/helpers/humanTime';
|
||||
import ItemList from 'flarum/common/utils/ItemList';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import type Dialog from '../../common/models/Dialog';
|
||||
import { ModelIdentifier } from 'flarum/common/Model';
|
||||
|
||||
export interface IDialogListItemAttrs extends ComponentAttrs {
|
||||
dialog: Dialog;
|
||||
active?: boolean;
|
||||
actions?: boolean;
|
||||
}
|
||||
|
||||
export default class DialogListItem<CustomAttrs extends IDialogListItemAttrs = IDialogListItemAttrs> extends Component<CustomAttrs> {
|
||||
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
const dialog = this.attrs.dialog;
|
||||
|
||||
const recipient = dialog.recipient();
|
||||
const lastMessage = dialog.lastMessage();
|
||||
|
||||
return (
|
||||
<li
|
||||
className={classList('DialogListItem', {
|
||||
'DialogListItem--unread': dialog.unreadCount(),
|
||||
active: this.attrs.active,
|
||||
})}
|
||||
>
|
||||
<Link
|
||||
href={app.route.dialog(dialog)}
|
||||
className={classList('DialogListItem-button', {
|
||||
active: this.attrs.active,
|
||||
})}
|
||||
>
|
||||
<div className="DialogListItem-avatar">
|
||||
<Avatar user={recipient} />
|
||||
{!!dialog.unreadCount() && <div className="Bubble Bubble--primary">{dialog.unreadCount()}</div>}
|
||||
</div>
|
||||
<div className="DialogListItem-content">
|
||||
<div className="DialogListItem-title">
|
||||
{username(recipient)}
|
||||
{humanTime(dialog.lastMessageAt()!)}
|
||||
{this.attrs.actions && <div className="DialogListItem-actions">{this.actionItems().toArray()}</div>}
|
||||
</div>
|
||||
<div className="DialogListItem-lastMessage">{lastMessage ? lastMessage.contentPlain()?.slice(0, 80) : ''}</div>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
actionItems(): ItemList<Mithril.Children> {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
|
||||
items.add(
|
||||
'markAsRead',
|
||||
<Button
|
||||
className="Notification-action Button Button--link"
|
||||
icon="fas fa-check"
|
||||
aria-label={app.translator.trans('flarum-messages.forum.dialog_list.mark_as_read_tooltip')}
|
||||
onclick={(e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.attrs.dialog
|
||||
.save({ lastReadMessageId: (this.attrs.dialog.data.relationships?.lastMessage.data as ModelIdentifier).id })
|
||||
.finally(() => {
|
||||
if (this.attrs.dialog.unreadCount() === 0) {
|
||||
app.session.user!.pushAttributes({
|
||||
messageCount: (app.session.user!.attribute<number>('messageCount') ?? 1) - 1,
|
||||
});
|
||||
}
|
||||
|
||||
m.redraw();
|
||||
});
|
||||
}}
|
||||
/>,
|
||||
100
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import Component, { type ComponentAttrs } from 'flarum/common/Component';
|
||||
import Dialog from '../../common/models/Dialog';
|
||||
import type Mithril from 'mithril';
|
||||
import MessageStream from './MessageStream';
|
||||
import username from 'flarum/common/helpers/username';
|
||||
import MessageStreamState from '../states/MessageStreamState';
|
||||
import Avatar from 'flarum/common/components/Avatar';
|
||||
import Link from 'flarum/common/components/Link';
|
||||
import app from 'flarum/forum/app';
|
||||
import ItemList from 'flarum/common/utils/ItemList';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import Dropdown from 'flarum/common/components/Dropdown';
|
||||
import DetailsModal from './DetailsModal';
|
||||
import listItems from 'flarum/common/helpers/listItems';
|
||||
|
||||
export interface IDialogStreamAttrs extends ComponentAttrs {
|
||||
dialog: Dialog;
|
||||
}
|
||||
|
||||
export default class DialogSection<CustomAttrs extends IDialogStreamAttrs = IDialogStreamAttrs> extends Component<CustomAttrs> {
|
||||
protected loading = false;
|
||||
protected messages!: MessageStreamState;
|
||||
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.messages = new MessageStreamState({
|
||||
filter: {
|
||||
dialog: this.attrs.dialog.id(),
|
||||
},
|
||||
sort: '-createdAt',
|
||||
});
|
||||
|
||||
this.messages.refresh();
|
||||
}
|
||||
|
||||
view() {
|
||||
const recipient = this.attrs.dialog.recipient();
|
||||
|
||||
return (
|
||||
<div className="DialogSection">
|
||||
<div className="DialogSection-header">
|
||||
<Avatar user={recipient} />
|
||||
<div className="DialogSection-header-info">
|
||||
{(recipient && (
|
||||
<Link href={app.route.user(recipient!)}>
|
||||
<h2>{username(recipient)}</h2>
|
||||
</Link>
|
||||
)) || <h2>{username(recipient)}</h2>}
|
||||
<div className="badges">{listItems(recipient?.badges().toArray() || [])}</div>
|
||||
</div>
|
||||
<div className="DialogSection-header-actions">{this.actionItems().toArray()}</div>
|
||||
</div>
|
||||
<MessageStream dialog={this.attrs.dialog} state={this.messages} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
actionItems() {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
|
||||
items.add(
|
||||
'details',
|
||||
<Dropdown
|
||||
icon="fas fa-ellipsis-h"
|
||||
className="DialogSection-controls"
|
||||
buttonClassName="Button Button--icon"
|
||||
accessibleToggleLabel={app.translator.trans('flarum-messages.forum.dialog_section.controls_toggle_label')}
|
||||
label={app.translator.trans('flarum-messages.forum.dialog_section.controls_toggle_label')}
|
||||
>
|
||||
{this.controlItems().toArray()}
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
controlItems() {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
|
||||
items.add(
|
||||
'details',
|
||||
<Button icon="fas fa-info-circle" onclick={() => app.modal.show(DetailsModal, { dialog: this.attrs.dialog })}>
|
||||
{app.translator.trans('flarum-messages.forum.dialog_section.controls.details_button')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import app from 'flarum/forum/app';
|
||||
import HeaderDropdown from 'flarum/forum/components/HeaderDropdown';
|
||||
import type { IHeaderDropdownAttrs } from 'flarum/forum/components/HeaderDropdown';
|
||||
import classList from 'flarum/common/utils/classList';
|
||||
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
|
||||
|
||||
export interface IDialogsDropdownAttrs extends IHeaderDropdownAttrs {}
|
||||
|
||||
export default class DialogsDropdown<CustomAttrs extends IDialogsDropdownAttrs = IDialogsDropdownAttrs> extends HeaderDropdown<CustomAttrs> {
|
||||
protected DialogDropdownList: any = null;
|
||||
|
||||
static initAttrs(attrs: IDialogsDropdownAttrs) {
|
||||
attrs.className = classList('DialogsDropdown', attrs.className);
|
||||
attrs.label = attrs.label || app.translator.trans('flarum-messages.forum.header.dropdown_tooltip');
|
||||
attrs.icon = attrs.icon || 'fas fa-envelope';
|
||||
|
||||
super.initAttrs(attrs);
|
||||
}
|
||||
|
||||
getContent() {
|
||||
if (!this.DialogDropdownList) {
|
||||
import('./DialogDropdownList').then((DialogDropdownList) => {
|
||||
this.DialogDropdownList = DialogDropdownList.default;
|
||||
});
|
||||
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
return <this.DialogDropdownList state={this.attrs.state} />;
|
||||
}
|
||||
|
||||
goToRoute() {
|
||||
m.route.set(app.route('dialogs'));
|
||||
}
|
||||
|
||||
getUnreadCount() {
|
||||
return app.session.user!.attribute<number>('messageCount');
|
||||
}
|
||||
|
||||
getNewCount() {
|
||||
return app.session.user!.attribute<number>('messageCount');
|
||||
}
|
||||
}
|
112
extensions/messages/js/src/forum/components/Message.tsx
Normal file
112
extensions/messages/js/src/forum/components/Message.tsx
Normal file
|
@ -0,0 +1,112 @@
|
|||
import app from 'flarum/forum/app';
|
||||
import ItemList from 'flarum/common/utils/ItemList';
|
||||
import Mithril from 'mithril';
|
||||
import AbstractPost, { type IAbstractPostAttrs } from 'flarum/forum/components/AbstractPost';
|
||||
import type User from 'flarum/common/models/User';
|
||||
import DialogMessage from '../../common/models/DialogMessage';
|
||||
import Avatar from 'flarum/common/components/Avatar';
|
||||
import Comment from 'flarum/forum/components/Comment';
|
||||
import PostUser from 'flarum/forum/components/PostUser';
|
||||
import PostMeta from 'flarum/forum/components/PostMeta';
|
||||
import classList from 'flarum/common/utils/classList';
|
||||
|
||||
export interface IMessageAttrs extends IAbstractPostAttrs {
|
||||
message: DialogMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `Post` component displays a single post. The basic post template just
|
||||
* includes a controls dropdown; subclasses must implement `content` and `attrs`
|
||||
* methods.
|
||||
*/
|
||||
export default abstract class Message<CustomAttrs extends IMessageAttrs = IMessageAttrs> extends AbstractPost<CustomAttrs> {
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
}
|
||||
|
||||
user(): User | null | false {
|
||||
return this.attrs.message.user();
|
||||
}
|
||||
|
||||
controls(): Mithril.Children[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
freshness(): Date {
|
||||
return this.attrs.message.freshness;
|
||||
}
|
||||
|
||||
createdByStarter(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
onbeforeupdate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||
return super.onbeforeupdate(vnode);
|
||||
}
|
||||
|
||||
onupdate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||
super.onupdate(vnode);
|
||||
}
|
||||
|
||||
elementAttrs() {
|
||||
const message = this.attrs.message;
|
||||
const attrs = super.elementAttrs();
|
||||
|
||||
attrs.className = classList(attrs.className || null, 'Message', {
|
||||
'Post--renderFailed': message.renderFailed(),
|
||||
revealContent: false,
|
||||
editing: false,
|
||||
});
|
||||
|
||||
return attrs;
|
||||
}
|
||||
|
||||
header(): Mithril.Children {
|
||||
return super.header();
|
||||
}
|
||||
|
||||
content(): Mithril.Children[] {
|
||||
return super
|
||||
.content()
|
||||
.concat([
|
||||
<Comment
|
||||
headerItems={this.headerItems()}
|
||||
cardVisible={false}
|
||||
isEditing={false}
|
||||
isHidden={false}
|
||||
contentHtml={this.attrs.message.contentHtml()}
|
||||
user={this.attrs.message.user()}
|
||||
/>,
|
||||
]);
|
||||
}
|
||||
|
||||
classes(existing?: string): string[] {
|
||||
return super.classes(existing);
|
||||
}
|
||||
|
||||
actionItems(): ItemList<Mithril.Children> {
|
||||
return super.actionItems();
|
||||
}
|
||||
|
||||
footerItems(): ItemList<Mithril.Children> {
|
||||
return super.footerItems();
|
||||
}
|
||||
|
||||
sideItems(): ItemList<Mithril.Children> {
|
||||
return super.sideItems();
|
||||
}
|
||||
|
||||
avatar(): Mithril.Children {
|
||||
return this.attrs.message.user() ? <Avatar user={this.attrs.message.user()} /> : '';
|
||||
}
|
||||
|
||||
headerItems() {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
const message = this.attrs.message;
|
||||
|
||||
items.add('user', <PostUser post={message} />, 100);
|
||||
items.add('meta', <PostMeta post={message} />);
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
143
extensions/messages/js/src/forum/components/MessageComposer.tsx
Normal file
143
extensions/messages/js/src/forum/components/MessageComposer.tsx
Normal file
|
@ -0,0 +1,143 @@
|
|||
import app from 'flarum/forum/app';
|
||||
import ComposerBody, { IComposerBodyAttrs } from 'flarum/forum/components/ComposerBody';
|
||||
import extractText from 'flarum/common/utils/extractText';
|
||||
import Stream from 'flarum/common/utils/Stream';
|
||||
import type User from 'flarum/common/models/User';
|
||||
import type Mithril from 'mithril';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import UserSelectionModal from 'flarum/common/components/UserSelectionModal';
|
||||
import DialogMessage from '../../common/models/DialogMessage';
|
||||
import Avatar from 'flarum/common/components/Avatar';
|
||||
import Tooltip from 'flarum/common/components/Tooltip';
|
||||
import type Dialog from '../../common/models/Dialog';
|
||||
|
||||
export interface IMessageComposerAttrs extends IComposerBodyAttrs {
|
||||
replyingTo?: Dialog;
|
||||
onsubmit?: (message: DialogMessage) => void;
|
||||
recipients?: User[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The `MessageComposer` component displays the composer content for sending
|
||||
* a new message. It adds a selection field as a header control so the user can
|
||||
* enter the recipient(s) of their message.
|
||||
*/
|
||||
export default class MessageComposer<CustomAttrs extends IMessageComposerAttrs = IMessageComposerAttrs> extends ComposerBody<CustomAttrs> {
|
||||
protected recipients!: Stream<User[]>;
|
||||
|
||||
static focusOnSelector = () => '.TextEditor-editor';
|
||||
|
||||
static initAttrs(attrs: IMessageComposerAttrs) {
|
||||
super.initAttrs(attrs);
|
||||
|
||||
attrs.placeholder = attrs.placeholder || extractText(app.translator.trans('flarum-messages.forum.composer.placeholder', {}, true));
|
||||
attrs.submitLabel = attrs.submitLabel || app.translator.trans('flarum-messages.forum.composer.submit_button', {}, true);
|
||||
attrs.confirmExit = attrs.confirmExit || extractText(app.translator.trans('flarum-messages.forum.composer.discard_confirmation', {}, true));
|
||||
attrs.className = 'ComposerBody--message';
|
||||
}
|
||||
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
|
||||
let users = this.attrs.replyingTo?.users() || this.attrs.recipients || [];
|
||||
|
||||
if (users) {
|
||||
users = users.filter((user) => user && user.id() !== app.session.user!.id());
|
||||
}
|
||||
|
||||
this.composer.fields.recipients = this.composer.fields.recipients || Stream(users);
|
||||
|
||||
this.recipients = this.composer.fields.recipients;
|
||||
}
|
||||
|
||||
headerItems() {
|
||||
const items = super.headerItems();
|
||||
|
||||
items.add(
|
||||
'recipients',
|
||||
<div className="MessageComposer-recipients">
|
||||
{!this.attrs.replyingTo && (
|
||||
<Button
|
||||
type="button"
|
||||
className="Button Button--outline Button--compact"
|
||||
onclick={() =>
|
||||
app.modal.show(UserSelectionModal, {
|
||||
title: app.translator.trans('flarum-messages.forum.recipient_selection_modal.title', {}, true),
|
||||
selected: this.recipients(),
|
||||
maxItems: 1,
|
||||
excluded: [app.session.user!.id()!],
|
||||
onsubmit: (users: User[]) => {
|
||||
this.recipients(users);
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{app.translator.trans('flarum-messages.forum.composer.recipients')}
|
||||
</Button>
|
||||
)}
|
||||
{!!this.recipients().length && (
|
||||
<div className="MessageComposer-recipients-label">{app.translator.trans('flarum-messages.forum.composer.to')}</div>
|
||||
)}
|
||||
<ul className="MessageComposer-recipients-list">
|
||||
{this.recipients().map((user) => (
|
||||
<li>
|
||||
<Tooltip text={user.username()}>
|
||||
<Avatar user={user} />
|
||||
</Tooltip>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>,
|
||||
100
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data to submit to the server when the discussion is saved.
|
||||
*/
|
||||
data(): Record<string, unknown> {
|
||||
const data: any = {
|
||||
content: this.composer.fields.content(),
|
||||
};
|
||||
|
||||
if (this.attrs.replyingTo) {
|
||||
data.relationships = {
|
||||
dialog: {
|
||||
data: {
|
||||
id: this.attrs.replyingTo.id(),
|
||||
type: 'dialogs',
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
data.users = this.recipients().map((user) => ({
|
||||
id: user.id(),
|
||||
}));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
onsubmit() {
|
||||
this.loading = true;
|
||||
|
||||
const data = this.data();
|
||||
|
||||
app.store
|
||||
.createRecord<DialogMessage>('dialog-messages')
|
||||
.save(data, {
|
||||
params: {
|
||||
include: ['dialog'],
|
||||
},
|
||||
})
|
||||
.then((message) => {
|
||||
this.composer.hide();
|
||||
// @todo: app.dialogs.refresh();
|
||||
// @ts-ignore
|
||||
m.route.set(app.route('dialog', { id: message.data.relationships!.dialog.data.id }));
|
||||
this.attrs.onsubmit?.(message);
|
||||
}, this.loaded.bind(this));
|
||||
}
|
||||
}
|
238
extensions/messages/js/src/forum/components/MessageStream.tsx
Normal file
238
extensions/messages/js/src/forum/components/MessageStream.tsx
Normal file
|
@ -0,0 +1,238 @@
|
|||
import app from 'flarum/forum/app';
|
||||
import Component, { type ComponentAttrs } from 'flarum/common/Component';
|
||||
import Mithril from 'mithril';
|
||||
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
|
||||
import MessageStreamState from '../states/MessageStreamState';
|
||||
import DialogMessage from '../../common/models/DialogMessage';
|
||||
import Stream from 'flarum/common/utils/Stream';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import { ModelIdentifier } from 'flarum/common/Model';
|
||||
import ScrollListener from 'flarum/common/utils/ScrollListener';
|
||||
import Dialog from '../../common/models/Dialog';
|
||||
import Message from './Message';
|
||||
|
||||
export interface IDialogStreamAttrs extends ComponentAttrs {
|
||||
dialog: Dialog;
|
||||
state: MessageStreamState;
|
||||
}
|
||||
|
||||
export default class MessageStream<CustomAttrs extends IDialogStreamAttrs = IDialogStreamAttrs> extends Component<CustomAttrs> {
|
||||
protected replyPlaceholderComponent = Stream<any>(null);
|
||||
protected loadingPostComponent = Stream<any>(null);
|
||||
protected scrollListener!: ScrollListener;
|
||||
protected initialToBottomScroll = false;
|
||||
protected lastTime: Date | null = null;
|
||||
protected checkedRead = false;
|
||||
protected markingAsRead = false;
|
||||
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
|
||||
// We need the lazy ReplyPlaceholder and LoadingPost components to be loaded.
|
||||
Promise.all([import('flarum/forum/components/ReplyPlaceholder'), import('flarum/forum/components/LoadingPost')]).then(
|
||||
([ReplyPlaceholder, LoadingPost]) => {
|
||||
this.replyPlaceholderComponent(ReplyPlaceholder.default);
|
||||
this.loadingPostComponent(LoadingPost.default);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
oncreate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
this.scrollListener = new ScrollListener(this.onscroll.bind(this), this.element);
|
||||
|
||||
setTimeout(() => {
|
||||
this.scrollListener.start();
|
||||
this.element.addEventListener('scrollend', this.markAsRead.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
onupdate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||
super.onupdate(vnode);
|
||||
|
||||
// @todo: for future versions, consider using the post stream scrubber to scroll through the messages. (big task..)
|
||||
// @todo: introduce read status, to jump to the first unread message instead.
|
||||
if (!this.initialToBottomScroll && !this.attrs.state.isLoading()) {
|
||||
this.scrollToBottom();
|
||||
this.initialToBottomScroll = true;
|
||||
}
|
||||
|
||||
if (this.initialToBottomScroll && !this.checkedRead) {
|
||||
this.markAsRead();
|
||||
this.checkedRead = true;
|
||||
}
|
||||
}
|
||||
|
||||
onremove(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||
super.onremove(vnode);
|
||||
|
||||
this.scrollListener.stop();
|
||||
}
|
||||
|
||||
view() {
|
||||
return <div className="MessageStream">{this.attrs.state.isLoading() ? <LoadingIndicator /> : this.content()}</div>;
|
||||
}
|
||||
|
||||
content() {
|
||||
const items: Mithril.Children[] = [];
|
||||
|
||||
const messages = this.attrs.state.getAllItems().sort((a, b) => a.createdAt().getTime() - b.createdAt().getTime());
|
||||
|
||||
const ReplyPlaceholder = this.replyPlaceholderComponent();
|
||||
const LoadingPost = this.loadingPostComponent();
|
||||
|
||||
if (messages[0].id() !== (this.attrs.dialog.data.relationships?.firstMessage.data as ModelIdentifier).id) {
|
||||
items.push(
|
||||
<div className="MessageStream-item" key="loadPrevious">
|
||||
<Button
|
||||
onclick={() => this.whileMaintainingScroll(() => this.attrs.state.loadNext())}
|
||||
type="button"
|
||||
className="Button Button--block MessageStream-loadPrev"
|
||||
>
|
||||
{app.translator.trans('flarum-messages.forum.messages_page.stream.load_previous_button')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (LoadingPost) {
|
||||
items.push(
|
||||
<div className="MessageStream-item" key="loading-prev">
|
||||
<LoadingPost />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
messages.forEach((message, index) => items.push(this.messageItem(message, index)));
|
||||
|
||||
if (ReplyPlaceholder) {
|
||||
items.push(
|
||||
<div className="MessageStream-item" key="reply" /*data-index={this.attrs.state.count()}*/>
|
||||
<ReplyPlaceholder
|
||||
discussion={this.attrs.dialog}
|
||||
onclick={() => {
|
||||
import('flarum/forum/components/ComposerBody').then(() => {
|
||||
app.composer
|
||||
.load(() => import('./MessageComposer'), {
|
||||
user: app.session.user,
|
||||
replyingTo: this.attrs.dialog,
|
||||
onsubmit: (message: DialogMessage) => {
|
||||
this.attrs.state.push(message);
|
||||
setTimeout(() => this.scrollToBottom(), 50);
|
||||
},
|
||||
})
|
||||
.then(() => app.composer.show());
|
||||
});
|
||||
}}
|
||||
composingReply={() => app.composer.composingMessageTo(this.attrs.dialog)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
messageItem(message: DialogMessage, index: number) {
|
||||
return (
|
||||
<div className="MessageStream-item" key={index} data-id={message.id()}>
|
||||
{this.timeGap(message)}
|
||||
<Message message={message} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
timeGap(message: DialogMessage): Mithril.Children {
|
||||
if (message.id() === (this.attrs.dialog.data.relationships?.firstMessage.data as ModelIdentifier).id) {
|
||||
this.lastTime = message.createdAt()!;
|
||||
|
||||
return (
|
||||
<div class="PostStream-timeGap">
|
||||
<span>{app.translator.trans('flarum-messages.forum.messages_page.stream.start_of_the_conversation')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const lastTime = this.lastTime;
|
||||
const dt = message.createdAt().getTime() - (lastTime?.getTime() || 0);
|
||||
this.lastTime = message.createdAt()!;
|
||||
|
||||
if (lastTime && dt > 1000 * 60 * 60 * 24 * 4) {
|
||||
return (
|
||||
<div className="PostStream-timeGap">
|
||||
{/* @ts-ignore */}
|
||||
<span>
|
||||
{app.translator.trans('flarum-messages.forum.messages_page.stream.time_lapsed_text', { period: dayjs().add(dt, 'ms').fromNow(true) })}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
onscroll() {
|
||||
this.whileMaintainingScroll(() => {
|
||||
if (this.element.scrollTop <= 80 && this.attrs.state.hasNext()) {
|
||||
return this.attrs.state.loadNext();
|
||||
}
|
||||
|
||||
if (this.element.scrollTop + this.element.clientHeight === this.element.scrollHeight && this.attrs.state.hasPrev()) {
|
||||
return this.attrs.state.loadPrev();
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
this.element.scrollTop = this.element.scrollHeight;
|
||||
}
|
||||
|
||||
whileMaintainingScroll(callback: () => null | Promise<void>) {
|
||||
const scrollTop = this.element.scrollTop;
|
||||
const scrollHeight = this.element.scrollHeight;
|
||||
|
||||
const result = callback();
|
||||
|
||||
if (result instanceof Promise) {
|
||||
result.then(() => {
|
||||
requestAnimationFrame(() => {
|
||||
this.element.scrollTop = this.element.scrollHeight - scrollHeight + scrollTop;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
markAsRead(): void {
|
||||
const lastVisibleId = Number(
|
||||
this.$('.MessageStream-item[data-id]')
|
||||
.filter((_, $el) => {
|
||||
if (this.element.scrollHeight <= this.element.clientHeight) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.$().offset()!.top + this.element.clientHeight > $($el).offset()!.top;
|
||||
})
|
||||
.last()
|
||||
.data('id')
|
||||
);
|
||||
|
||||
if (lastVisibleId && app.session.user && lastVisibleId > (this.attrs.dialog.lastReadMessageId() || 0) && !this.markingAsRead) {
|
||||
this.markingAsRead = true;
|
||||
|
||||
this.attrs.dialog.save({ lastReadMessageId: lastVisibleId }).finally(() => {
|
||||
this.markingAsRead = false;
|
||||
|
||||
if (this.attrs.dialog.unreadCount() === 0) {
|
||||
app.session.user!.pushAttributes({
|
||||
messageCount: (app.session.user!.attribute<number>('messageCount') ?? 1) - 1,
|
||||
});
|
||||
}
|
||||
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
204
extensions/messages/js/src/forum/components/MessagesPage.tsx
Normal file
204
extensions/messages/js/src/forum/components/MessagesPage.tsx
Normal file
|
@ -0,0 +1,204 @@
|
|||
import app from 'flarum/forum/app';
|
||||
import Page, { IPageAttrs } from 'flarum/common/components/Page';
|
||||
import PageStructure from 'flarum/forum/components/PageStructure';
|
||||
import Mithril from 'mithril';
|
||||
import Icon from 'flarum/common/components/Icon';
|
||||
import DialogList from './DialogList';
|
||||
import Dialog from '../../common/models/Dialog';
|
||||
import LoadingIndicator from 'flarum/common/components/LoadingIndicator';
|
||||
import Stream from 'flarum/common/utils/Stream';
|
||||
import InfoTile from 'flarum/common/components/InfoTile';
|
||||
import MessagesSidebar from './MessagesSidebar';
|
||||
import DialogSection from './DialogSection';
|
||||
import listItems from 'flarum/common/helpers/listItems';
|
||||
import ItemList from 'flarum/common/utils/ItemList';
|
||||
import Dropdown from 'flarum/common/components/Dropdown';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
|
||||
export interface IMessagesPageAttrs extends IPageAttrs {}
|
||||
|
||||
export default class MessagesPage<CustomAttrs extends IMessagesPageAttrs = IMessagesPageAttrs> extends Page<CustomAttrs> {
|
||||
protected selectedDialog = Stream<Dialog | null>(null);
|
||||
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
|
||||
if (!app.session.user) {
|
||||
m.route.set(app.route('index'));
|
||||
return;
|
||||
}
|
||||
|
||||
app.current.set('noTagsList', true);
|
||||
|
||||
if (!app.dialogs.hasItems()) {
|
||||
app.dialogs.refresh().then(async () => {
|
||||
if (app.dialogs.hasItems()) {
|
||||
await this.initDialog();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.initDialog();
|
||||
}
|
||||
}
|
||||
|
||||
dialogRequestParams() {
|
||||
return {
|
||||
include: 'users.groups',
|
||||
};
|
||||
}
|
||||
|
||||
protected async initDialog() {
|
||||
const dialogId = m.route.param('id');
|
||||
|
||||
const title = app.translator.trans('flarum-messages.forum.messages_page.title', {}, true);
|
||||
|
||||
let dialog: Dialog | null;
|
||||
|
||||
if (dialogId) {
|
||||
dialog =
|
||||
app.store.getById<Dialog>('dialogs', dialogId) || ((await app.store.find<Dialog>('dialogs', dialogId, this.dialogRequestParams())) as Dialog);
|
||||
} else {
|
||||
dialog = app.dialogs.getAllItems()[0];
|
||||
}
|
||||
|
||||
this.selectedDialog(dialog);
|
||||
|
||||
if (dialog) {
|
||||
app.setTitle(dialog.title());
|
||||
app.history.push('dialog', dialog.title());
|
||||
} else {
|
||||
app.setTitle(title);
|
||||
app.history.push('messages', title);
|
||||
}
|
||||
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
onupdate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||
super.onupdate(vnode);
|
||||
|
||||
// Scroll the dialog list to the active dialog item if present and not visible.
|
||||
const dialogElement = this.element.querySelector('.DialogListItem.active');
|
||||
const container = this.element.querySelector('.DialogList')!;
|
||||
|
||||
if (dialogElement && $(container).offset()!.top + container.clientHeight <= $(dialogElement).offset()!.top) {
|
||||
dialogElement.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
view() {
|
||||
return (
|
||||
<PageStructure className="MessagesPage Page--vertical" loading={false} hero={this.hero.bind(this)} sidebar={() => <MessagesSidebar />}>
|
||||
{app.dialogs.isLoading() ? (
|
||||
<LoadingIndicator />
|
||||
) : !app.dialogs.hasItems() ? (
|
||||
<InfoTile icon="far fa-envelope-open">{app.translator.trans('flarum-messages.forum.messages_page.empty_text')}</InfoTile>
|
||||
) : (
|
||||
<div className="MessagesPage-content">
|
||||
<div className="MessagesPage-sidebar" key="sidebar">
|
||||
<div className="IndexPage-toolbar" key="toolbar">
|
||||
<ul className="IndexPage-toolbar-view">{listItems(this.viewItems().toArray())}</ul>
|
||||
<ul className="IndexPage-toolbar-action">{listItems(this.actionItems().toArray())}</ul>
|
||||
</div>
|
||||
<DialogList key="list" state={app.dialogs} activeDialog={this.selectedDialog()} />
|
||||
</div>
|
||||
{this.selectedDialog() ? (
|
||||
<DialogSection key="dialog" dialog={this.selectedDialog()} />
|
||||
) : (
|
||||
<LoadingIndicator key="loading" display="block" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PageStructure>
|
||||
);
|
||||
}
|
||||
|
||||
hero(): Mithril.Children {
|
||||
return (
|
||||
<header className="Hero MessagesPageHero">
|
||||
<div className="container">
|
||||
<div className="containerNarrow">
|
||||
<h1 className="Hero-title">
|
||||
<Icon name="fas fa-envelope" /> {app.translator.trans('flarum-messages.forum.messages_page.hero.title')}
|
||||
</h1>
|
||||
<div className="Hero-subtitle">{app.translator.trans('flarum-messages.forum.messages_page.hero.subtitle')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the part of the toolbar which is concerned with how
|
||||
* the results are displayed. By default this is just a select box to change
|
||||
* the way discussions are sorted.
|
||||
*/
|
||||
viewItems() {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
const sortMap = app.dialogs.sortMap();
|
||||
|
||||
const sortOptions = Object.keys(sortMap).reduce((acc: any, sortId) => {
|
||||
const sort = sortMap[sortId];
|
||||
acc[sortId] = typeof sort !== 'string' ? sort.label : app.translator.trans(`flarum-messages.forum.index_sort.${sortId}_button`);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
items.add(
|
||||
'sort',
|
||||
<Dropdown
|
||||
buttonClassName="Button"
|
||||
label={sortOptions[app.dialogs.getParams()?.sort || 0] || Object.values(sortOptions)[0]}
|
||||
accessibleToggleLabel={app.translator.trans('core.forum.index_sort.toggle_dropdown_accessible_label')}
|
||||
>
|
||||
{Object.keys(sortOptions).map((value) => {
|
||||
const label = sortOptions[value];
|
||||
const active = (app.dialogs.getParams().sort || Object.keys(sortMap)[0]) === value;
|
||||
|
||||
return (
|
||||
<Button icon={active ? 'fas fa-check' : true} onclick={() => app.dialogs.changeSort(value)} active={active}>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an item list for the part of the toolbar which is about taking action
|
||||
* on the results. By default this is just a "mark all as read" button.
|
||||
*/
|
||||
actionItems() {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
|
||||
items.add(
|
||||
'refresh',
|
||||
<Button
|
||||
title={app.translator.trans('flarum-messages.forum.messages_page.refresh_tooltip')}
|
||||
aria-label={app.translator.trans('flarum-messages.forum.messages_page.refresh_tooltip')}
|
||||
icon="fas fa-sync"
|
||||
className="Button Button--icon"
|
||||
onclick={() => {
|
||||
app.dialogs.refresh();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (app.session.user) {
|
||||
items.add(
|
||||
'markAllAsRead',
|
||||
<Button
|
||||
title={app.translator.trans('flarum-messages.forum.messages_page.mark_all_as_read_tooltip')}
|
||||
aria-label={app.translator.trans('flarum-messages.forum.messages_page.mark_all_as_read_tooltip')}
|
||||
icon="fas fa-check"
|
||||
className="Button Button--icon"
|
||||
onclick={() => app.dialogs.markAllAsRead()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import app from 'flarum/forum/app';
|
||||
import IndexSidebar, { type IndexSidebarAttrs } from 'flarum/forum/components/IndexSidebar';
|
||||
import Mithril from 'mithril';
|
||||
import ItemList from 'flarum/common/utils/ItemList';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
|
||||
export interface IMessagesSidebarAttrs extends IndexSidebarAttrs {}
|
||||
|
||||
export default class MessagesSidebar<CustomAttrs extends IMessagesSidebarAttrs = IMessagesSidebarAttrs> extends IndexSidebar<CustomAttrs> {
|
||||
static initAttrs(attrs: IMessagesSidebarAttrs) {
|
||||
attrs.className = 'MessagesPage-nav';
|
||||
}
|
||||
|
||||
items(): ItemList<Mithril.Children> {
|
||||
const items = super.items();
|
||||
|
||||
const canSendAnyMessage = app.session.user!.attribute<boolean>('canSendAnyMessage');
|
||||
|
||||
items.remove('newDiscussion');
|
||||
|
||||
items.add(
|
||||
'newMessage',
|
||||
<Button
|
||||
icon="fas fa-edit"
|
||||
className="Button Button--primary IndexPage-newDiscussion MessagesPage-newMessage"
|
||||
itemClassName="App-primaryControl"
|
||||
onclick={() => {
|
||||
return this.newMessageAction();
|
||||
}}
|
||||
disabled={!canSendAnyMessage}
|
||||
>
|
||||
{app.translator.trans('flarum-messages.forum.messages_page.new_message_button')}
|
||||
</Button>,
|
||||
10
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the composer for a new message.
|
||||
*/
|
||||
newMessageAction(): Promise<unknown> {
|
||||
return import('flarum/forum/components/ComposerBody').then(() => {
|
||||
app.composer
|
||||
.load(() => import('./MessageComposer'), {
|
||||
user: app.session.user,
|
||||
onsubmit: () => {
|
||||
app.dialogs.refresh();
|
||||
},
|
||||
})
|
||||
.then(() => app.composer.show());
|
||||
|
||||
return app.composer;
|
||||
});
|
||||
}
|
||||
}
|
13
extensions/messages/js/src/forum/extend.ts
Normal file
13
extensions/messages/js/src/forum/extend.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import app from 'flarum/forum/app';
|
||||
import Extend from 'flarum/common/extenders';
|
||||
import commonExtend from '../common/extend';
|
||||
import type Dialog from '../common/models/Dialog';
|
||||
|
||||
export default [
|
||||
...commonExtend,
|
||||
|
||||
new Extend.Routes() //
|
||||
.add('messages', '/messages', () => import('./components/MessagesPage'))
|
||||
.add('dialog', '/messages/dialog/:id', () => import('./components/MessagesPage'))
|
||||
.helper('dialog', (dialog: Dialog) => app.route('dialog', { id: dialog.id() })),
|
||||
];
|
87
extensions/messages/js/src/forum/index.tsx
Normal file
87
extensions/messages/js/src/forum/index.tsx
Normal file
|
@ -0,0 +1,87 @@
|
|||
import app from 'flarum/forum/app';
|
||||
import { extend } from 'flarum/common/extend';
|
||||
import IndexSidebar from 'flarum/forum/components/IndexSidebar';
|
||||
import LinkButton from 'flarum/common/components/LinkButton';
|
||||
import HeaderSecondary from 'flarum/forum/components/HeaderSecondary';
|
||||
import UserControls from 'flarum/forum/utils/UserControls';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import type Dialog from '../common/models/Dialog';
|
||||
import DialogsDropdown from './components/DialogsDropdown';
|
||||
import DialogListState from './states/DialogListState';
|
||||
|
||||
export { default as extend } from './extend';
|
||||
|
||||
app.initializers.add('flarum-messages', () => {
|
||||
app.dialogs = new DialogListState({}, 1);
|
||||
app.dropdownDialogs = new DialogListState(
|
||||
{
|
||||
filter: {
|
||||
unread: true,
|
||||
},
|
||||
},
|
||||
1,
|
||||
5
|
||||
);
|
||||
|
||||
app.composer.composingMessageTo = function (dialog: Dialog) {
|
||||
const MessageComposer = flarum.reg.checkModule('flarum-messages', 'forum/components/MessageComposer');
|
||||
|
||||
if (!MessageComposer) return false;
|
||||
|
||||
return this.isVisible() && this.bodyMatches(MessageComposer, { dialog });
|
||||
};
|
||||
|
||||
extend(IndexSidebar.prototype, 'navItems', function (items) {
|
||||
if (app.session.user) {
|
||||
items.add(
|
||||
'messages',
|
||||
<LinkButton
|
||||
href={app.route('messages')}
|
||||
icon="far fa-envelope"
|
||||
active={app.current.data.routeName && ['messages', 'dialog'].includes(app.current.data.routeName)}
|
||||
>
|
||||
{app.translator.trans('flarum-messages.forum.index.messages_link')}
|
||||
</LinkButton>,
|
||||
95
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
extend(HeaderSecondary.prototype, 'items', function (items) {
|
||||
if (app.session.user?.attribute<boolean>('canSendAnyMessage')) {
|
||||
items.add('messages', <DialogsDropdown state={app.dropdownDialogs} />, 15);
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
extend(UserControls, 'userControls', (items, user) => {
|
||||
if (app.session.user?.attribute<boolean>('canSendAnyMessage')) {
|
||||
items.add(
|
||||
'sendMessage',
|
||||
<Button
|
||||
icon="fas fa-envelope"
|
||||
onclick={() => {
|
||||
import('flarum/forum/components/ComposerBody').then(() => {
|
||||
app.composer
|
||||
.load(() => import('./components/MessageComposer'), {
|
||||
user: app.session.user,
|
||||
recipients: [user],
|
||||
})
|
||||
.then(() => app.composer.show());
|
||||
});
|
||||
}}
|
||||
>
|
||||
{app.translator.trans('flarum-messages.forum.user_controls.send_message_button')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
extend('flarum/forum/components/NotificationGrid', 'notificationTypes', function (items) {
|
||||
items.add('messageReceived', {
|
||||
name: 'messageReceived',
|
||||
icon: 'fas fa-envelope',
|
||||
label: app.translator.trans('flarum-messages.forum.settings.notify_message_received_label'),
|
||||
});
|
||||
});
|
||||
});
|
75
extensions/messages/js/src/forum/states/DialogListState.ts
Normal file
75
extensions/messages/js/src/forum/states/DialogListState.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import app from 'flarum/forum/app';
|
||||
import PaginatedListState, { PaginatedListParams, type SortMap } from 'flarum/common/states/PaginatedListState';
|
||||
import Dialog from '../../common/models/Dialog';
|
||||
import { type PaginatedListRequestParams } from 'flarum/common/states/PaginatedListState';
|
||||
|
||||
export interface DialogListParams extends PaginatedListParams {
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
export default class DialogListState<P extends DialogListParams = DialogListParams> extends PaginatedListState<Dialog, P> {
|
||||
protected lastCount: number = 0;
|
||||
|
||||
constructor(params: P, page: number = 1, perPage: null | number = null) {
|
||||
super(params, page, perPage);
|
||||
}
|
||||
|
||||
get type(): string {
|
||||
return 'dialogs';
|
||||
}
|
||||
|
||||
public getAllItems(): Dialog[] {
|
||||
return super.getAllItems();
|
||||
}
|
||||
|
||||
requestParams(): PaginatedListRequestParams {
|
||||
const params = {
|
||||
include: ['lastMessage', 'users.groups'],
|
||||
filter: this.params.filter || {},
|
||||
sort: this.currentSort() || this.sortValue(Object.values(this.sortMap())[0]),
|
||||
};
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
sortMap(): SortMap {
|
||||
const map: any = {};
|
||||
|
||||
map.latest = '-lastMessageAt';
|
||||
map.newest = '-createdAt';
|
||||
map.oldest = 'createdAt';
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
load(): Promise<void> {
|
||||
if (app.session.user?.attribute<number>('messageCount') !== this.lastCount) {
|
||||
this.pages = [];
|
||||
this.location = { page: 1 };
|
||||
|
||||
this.lastCount = app.session.user?.attribute<number>('messageCount') || 0;
|
||||
}
|
||||
|
||||
if (this.pages.length > 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return super.loadNext();
|
||||
}
|
||||
|
||||
markAllAsRead() {
|
||||
return app
|
||||
.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/dialogs/read',
|
||||
})
|
||||
.then(() => {
|
||||
app.dialogs.getAllItems().forEach((dialog: Dialog) => {
|
||||
dialog.pushAttributes({ unreadCount: 0 });
|
||||
});
|
||||
app.session.user!.pushAttributes({ messageCount: 0 });
|
||||
app.dropdownDialogs.clear();
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import PaginatedListState, { PaginatedListParams } from 'flarum/common/states/PaginatedListState';
|
||||
import DialogMessage from '../../common/models/DialogMessage';
|
||||
|
||||
export interface MessageStreamParams extends PaginatedListParams {
|
||||
//
|
||||
}
|
||||
|
||||
export default class MessageStreamState<P extends MessageStreamParams = MessageStreamParams> extends PaginatedListState<DialogMessage, P> {
|
||||
constructor(params: P, page: number = 1) {
|
||||
super(params, page, null);
|
||||
}
|
||||
|
||||
get type(): string {
|
||||
return 'dialog-messages';
|
||||
}
|
||||
|
||||
public getAllItems(): DialogMessage[] {
|
||||
return super.getAllItems();
|
||||
}
|
||||
}
|
0
extensions/messages/js/tests/integration/.gitkeep
Normal file
0
extensions/messages/js/tests/integration/.gitkeep
Normal file
0
extensions/messages/js/tests/unit/.gitkeep
Normal file
0
extensions/messages/js/tests/unit/.gitkeep
Normal file
15
extensions/messages/js/tsconfig.json
Normal file
15
extensions/messages/js/tsconfig.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
// Use Flarum's tsconfig as a starting point
|
||||
"extends": "flarum-tsconfig",
|
||||
// This will match all .ts, .tsx, .d.ts, .js, .jsx files in your `src` folder
|
||||
// and also tells your Typescript server to read core's global typings for
|
||||
// access to `dayjs` and `$` in the global namespace.
|
||||
"include": ["src/**/*", "../../../framework/core/js/dist-typings/@types/**/*", "@types/**/*"],
|
||||
"compilerOptions": {
|
||||
// This will output typings to `dist-typings`
|
||||
"declarationDir": "./dist-typings",
|
||||
"paths": {
|
||||
"flarum/*": ["../../../framework/core/js/dist-typings/*"]
|
||||
}
|
||||
}
|
||||
}
|
10
extensions/messages/js/tsconfig.test.json
Normal file
10
extensions/messages/js/tsconfig.test.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["tests/**/*"],
|
||||
"files": ["../../../node_modules/@flarum/jest-config/shims.d.ts"],
|
||||
"compilerOptions": {
|
||||
"strict": false,
|
||||
"noImplicitReturns": false,
|
||||
"noImplicitAny": false
|
||||
}
|
||||
}
|
1
extensions/messages/js/webpack.config.cjs
Normal file
1
extensions/messages/js/webpack.config.cjs
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = require('flarum-webpack-config')();
|
0
extensions/messages/less/admin.less
Normal file
0
extensions/messages/less/admin.less
Normal file
242
extensions/messages/less/forum.less
Normal file
242
extensions/messages/less/forum.less
Normal file
|
@ -0,0 +1,242 @@
|
|||
.MessagesPage-sidebar {
|
||||
flex-shrink: 0;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.MessagesPage-content {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
|
||||
.Avatar {
|
||||
--size: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.MessageComposer-recipients {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--control-color);
|
||||
|
||||
.Avatar {
|
||||
--size: 24px;
|
||||
}
|
||||
|
||||
&-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.DialogList-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.Avatar {
|
||||
--size: 42px;
|
||||
}
|
||||
|
||||
.DialogDropdownList & {
|
||||
gap: 0
|
||||
}
|
||||
}
|
||||
|
||||
.DialogListItem {
|
||||
.CompactActions();
|
||||
|
||||
&-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: var(--text-color);
|
||||
text-decoration: none;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
&:hover, &.active {
|
||||
text-decoration: none;
|
||||
background-color: var(--control-bg);
|
||||
|
||||
.DialogListItem-lastMessage {
|
||||
color: var(--control-color);
|
||||
}
|
||||
}
|
||||
|
||||
.DialogDropdownList & {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
|
||||
.username {
|
||||
max-width: 56%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&-avatar {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
|
||||
.Bubble {
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
left: unset;
|
||||
height: 18px;
|
||||
min-width: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&-lastMessage {
|
||||
color: var(--muted-more-color);
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&--unread &-button:not(:hover) {
|
||||
background-color: var(--control-body-bg-mix);
|
||||
}
|
||||
|
||||
&--unread &-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&--unread &-lastMessage {
|
||||
color: var(--control-color);
|
||||
}
|
||||
|
||||
time {
|
||||
font-size: 11px;
|
||||
line-height: 19px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: var(--control-color);
|
||||
}
|
||||
|
||||
.DialogDropdownList &-title {
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.DialogDropdownList &-actions {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.DialogSection {
|
||||
flex-grow: 1;
|
||||
padding-inline-start: 32px;
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--control-bg);
|
||||
|
||||
a {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
&-actions {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
&-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Message {
|
||||
padding-right: 0;
|
||||
|
||||
.PostUser-badges {
|
||||
margin-left: -72px;
|
||||
}
|
||||
}
|
||||
|
||||
.MessageStream {
|
||||
--avatar-column-width: 55px;
|
||||
padding-inline-end: 0.5rem;
|
||||
margin-inline-start: -32px;
|
||||
padding-inline-start: 32px;
|
||||
padding-bottom: 1px;
|
||||
mask-image: linear-gradient(180deg, transparent 0%, var(--body-bg) 3%, var(--body-bg) 97%, transparent 100%);
|
||||
}
|
||||
|
||||
.MessageStream, .DialogList {
|
||||
max-height: calc(100vh - var(--header-height) - 140px - 235px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.DialogList-loadMore {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.DetailsModal-infoGroups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.DetailsModal-info {
|
||||
&-title {
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
color: var(--muted-more-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&-content {
|
||||
color: var(--control-color);
|
||||
}
|
||||
}
|
||||
|
||||
.DetailsModal-recipient {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.Avatar {
|
||||
--size: 38px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-color);
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.DetailsModal-recipients-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
89
extensions/messages/locale/en.yml
Normal file
89
extensions/messages/locale/en.yml
Normal file
|
@ -0,0 +1,89 @@
|
|||
flarum-messages:
|
||||
|
||||
# Translations in this namespace are used by the admin interface.
|
||||
admin:
|
||||
permissions:
|
||||
send_messages: Send private messages
|
||||
|
||||
# Translations in this namespace are used by the forum user interface.
|
||||
forum:
|
||||
composer:
|
||||
discard_confirmation: You have not sent your message. Are you sure you want to discard it?
|
||||
placeholder: Write a message...
|
||||
recipients: Recipients
|
||||
submit_button: Send message
|
||||
to: "To:"
|
||||
|
||||
dialog_list:
|
||||
load_more_button: => core.ref.load_more
|
||||
mark_as_read_tooltip: Mark as read
|
||||
title: Messages
|
||||
view_all: View all messages
|
||||
|
||||
dialog_section:
|
||||
controls:
|
||||
details_button: Details
|
||||
controls_toggle_label: Dialog control actions
|
||||
details_modal:
|
||||
created_at: Creation date
|
||||
recipients: Participants
|
||||
title: Conversation details
|
||||
|
||||
index:
|
||||
messages_link: Messages
|
||||
|
||||
index_sort:
|
||||
latest_button: Latest
|
||||
newest_button: Newest
|
||||
oldest_button: Oldest
|
||||
|
||||
messages_page:
|
||||
empty_text: You have no messages yet. When you send or receive messages, they
|
||||
will appear here.
|
||||
hero:
|
||||
title: Messages
|
||||
subtitle: Your private conversations with other users
|
||||
mark_all_as_read_tooltip: Mark all as read
|
||||
new_message_button: Send a Message
|
||||
refresh_tooltip: Refresh
|
||||
stream:
|
||||
load_previous_button: Load previous messages
|
||||
start_of_the_conversation: Start of the conversation
|
||||
time_lapsed_text: => core.forum.post_stream.time_lapsed_text
|
||||
title: Messages
|
||||
|
||||
recipient_selection_modal:
|
||||
title: Select the recipients of this message
|
||||
|
||||
settings:
|
||||
notify_message_received_label: Someone sends me a message
|
||||
|
||||
user_controls:
|
||||
send_message_button: Send a message
|
||||
|
||||
notifications:
|
||||
message_received_text: Message Received notification from {user}
|
||||
|
||||
# Translations in this namespace are used in emails sent by the forum.
|
||||
email:
|
||||
|
||||
# These translations are used in emails sent when a user is mentioned
|
||||
message_received:
|
||||
subject: "{user_display_name} sent you a message"
|
||||
plain:
|
||||
body: |
|
||||
{user_display_name} sent you a new message.
|
||||
|
||||
{url}
|
||||
|
||||
---
|
||||
|
||||
{content}
|
||||
html:
|
||||
body: "{user_display_name} sent you a [new message]({url})."
|
||||
|
||||
# Translations in this namespace are used by the forum and admin interfaces.
|
||||
lib:
|
||||
|
||||
dialog:
|
||||
title: Messaging {username}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
return Migration::createTable(
|
||||
'dialogs',
|
||||
function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->unsignedBigInteger('first_message_id')->nullable();
|
||||
$table->unsignedBigInteger('last_message_id')->nullable();
|
||||
$table->dateTime('last_message_at')->nullable();
|
||||
$table->unsignedInteger('last_message_user_id')->nullable();
|
||||
$table->foreign('last_message_user_id')->references('id')->on('users')->nullOnDelete();
|
||||
$table->string('type');
|
||||
$table->timestamps();
|
||||
}
|
||||
);
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
return Migration::createTable(
|
||||
'dialog_messages',
|
||||
function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->foreignId('dialog_id')->constrained()->cascadeOnDelete();
|
||||
$table->unsignedInteger('user_id')->nullable();
|
||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
$table->text('content');
|
||||
$table->timestamps();
|
||||
}
|
||||
);
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Database\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
return Migration::createTable(
|
||||
'dialog_user',
|
||||
function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('dialog_id')->constrained()->cascadeOnDelete();
|
||||
$table->unsignedInteger('user_id');
|
||||
$table->dateTime('joined_at');
|
||||
$table->unsignedBigInteger('last_read_message_id')->default(0);
|
||||
$table->dateTime('last_read_at')->nullable();
|
||||
$table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete();
|
||||
}
|
||||
);
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$schema->table('dialogs', function (Blueprint $table) {
|
||||
$table->foreign('first_message_id')->references('id')->on('dialog_messages')->nullOnDelete();
|
||||
$table->foreign('last_message_id')->references('id')->on('dialog_messages')->nullOnDelete();
|
||||
});
|
||||
},
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->table('dialogs', function (Blueprint $table) {
|
||||
$table->dropForeign(['first_message_id']);
|
||||
$table->dropForeign(['last_message_id']);
|
||||
});
|
||||
}
|
||||
];
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Flarum\Database\Migration;
|
||||
use Flarum\Group\Group;
|
||||
|
||||
return Migration::addPermissions([
|
||||
'dialog.sendMessage' => Group::MEMBER_ID,
|
||||
]);
|
22
extensions/messages/src/Access/DialogMessagePolicy.php
Normal file
22
extensions/messages/src/Access/DialogMessagePolicy.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Access;
|
||||
|
||||
use Flarum\Messages\DialogMessage;
|
||||
use Flarum\User\Access\AbstractPolicy;
|
||||
use Flarum\User\User;
|
||||
|
||||
class DialogMessagePolicy extends AbstractPolicy
|
||||
{
|
||||
public function update(User $actor, DialogMessage $dialogMessage): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
27
extensions/messages/src/Access/DialogPolicy.php
Normal file
27
extensions/messages/src/Access/DialogPolicy.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Access;
|
||||
|
||||
use Flarum\Messages\Dialog;
|
||||
use Flarum\User\Access\AbstractPolicy;
|
||||
use Flarum\User\User;
|
||||
|
||||
class DialogPolicy extends AbstractPolicy
|
||||
{
|
||||
public function view(User $actor, Dialog $dialog): bool
|
||||
{
|
||||
return Dialog::whereVisibleTo($actor)->where('id', $dialog->id)->exists();
|
||||
}
|
||||
|
||||
public function sendMessage(User $actor, Dialog $dialog): bool
|
||||
{
|
||||
return $this->view($actor, $dialog) && $actor->hasPermission('dialog.sendMessage');
|
||||
}
|
||||
}
|
21
extensions/messages/src/Access/GlobalPolicy.php
Normal file
21
extensions/messages/src/Access/GlobalPolicy.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Access;
|
||||
|
||||
use Flarum\User\Access\AbstractPolicy;
|
||||
use Flarum\User\User;
|
||||
|
||||
class GlobalPolicy extends AbstractPolicy
|
||||
{
|
||||
public function sendAnyMessage(User $actor): bool
|
||||
{
|
||||
return $actor->hasPermission('dialog.sendMessage');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Access;
|
||||
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ScopeDialogMessageVisibility
|
||||
{
|
||||
public function __invoke(User $actor, Builder $query): void
|
||||
{
|
||||
$query->whereHas('dialog', function (Builder $query) use ($actor) {
|
||||
$query->whereVisibleTo($actor);
|
||||
});
|
||||
}
|
||||
}
|
23
extensions/messages/src/Access/ScopeDialogVisibility.php
Normal file
23
extensions/messages/src/Access/ScopeDialogVisibility.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Access;
|
||||
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ScopeDialogVisibility
|
||||
{
|
||||
public function __invoke(User $actor, Builder $query): void
|
||||
{
|
||||
$query->whereHas('users', function (Builder $query) use ($actor) {
|
||||
$query->where('user_id', $actor->id);
|
||||
});
|
||||
}
|
||||
}
|
226
extensions/messages/src/Api/Resource/DialogMessageResource.php
Normal file
226
extensions/messages/src/Api/Resource/DialogMessageResource.php
Normal file
|
@ -0,0 +1,226 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Api\Resource;
|
||||
|
||||
use Exception;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Api\Sort\SortColumn;
|
||||
use Flarum\Bus\Dispatcher;
|
||||
use Flarum\Foundation\ErrorHandling\LogReporter;
|
||||
use Flarum\Foundation\ValidationException;
|
||||
use Flarum\Locale\Translator;
|
||||
use Flarum\Messages\Command\ReadDialog;
|
||||
use Flarum\Messages\Dialog;
|
||||
use Flarum\Messages\DialogMessage;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Tobyz\JsonApiServer\Context as OriginalContext;
|
||||
|
||||
/**
|
||||
* @extends Resource\AbstractDatabaseResource<DialogMessage>
|
||||
*/
|
||||
class DialogMessageResource extends Resource\AbstractDatabaseResource
|
||||
{
|
||||
public function __construct(
|
||||
protected Translator $translator,
|
||||
protected LogReporter $log,
|
||||
protected Dispatcher $bus,
|
||||
) {
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'dialog-messages';
|
||||
}
|
||||
|
||||
public function model(): string
|
||||
{
|
||||
return DialogMessage::class;
|
||||
}
|
||||
|
||||
public function scope(Builder $query, OriginalContext $context): void
|
||||
{
|
||||
$query->whereVisibleTo($context->getActor());
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [
|
||||
Endpoint\Create::make()
|
||||
->authenticated()
|
||||
->visible(function (Context $context): bool {
|
||||
$actor = $context->getActor();
|
||||
|
||||
$dialogId = (int) Arr::get($context->body(), 'data.relationships.dialog.data.id');
|
||||
|
||||
// If this is a new dialog instance, the user must have permission to
|
||||
// start new dialogs. Otherwise, they must have access to send messages in
|
||||
// this dialog.
|
||||
if ($dialogId) {
|
||||
$dialog = Dialog::whereVisibleTo($context->getActor())->findOrFail($dialogId);
|
||||
|
||||
return $actor->can('sendMessage', $dialog);
|
||||
} else {
|
||||
return $actor->can('sendAnyMessage');
|
||||
}
|
||||
}),
|
||||
Endpoint\Index::make()
|
||||
->authenticated()
|
||||
->paginate(),
|
||||
];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
return [
|
||||
|
||||
Schema\Str::make('content')
|
||||
->requiredOnCreate()
|
||||
->writableOnCreate()
|
||||
->hidden()
|
||||
->minLength(1)
|
||||
->maxLength(63000)
|
||||
->set(function (DialogMessage $post, string $value, Context $context) {
|
||||
$post->setContentAttribute($value, $context->getActor());
|
||||
}),
|
||||
Schema\Str::make('contentHtml')
|
||||
->get(function (DialogMessage $post, Context $context) {
|
||||
try {
|
||||
$rendered = $post->formatContent($context->request);
|
||||
$post->setAttribute('renderFailed', false);
|
||||
} catch (Exception $e) {
|
||||
$rendered = $this->translator->trans('core.lib.error.render_failed_message');
|
||||
$this->log->report($e);
|
||||
$post->setAttribute('renderFailed', true);
|
||||
}
|
||||
|
||||
return $rendered;
|
||||
}),
|
||||
Schema\Boolean::make('renderFailed'),
|
||||
Schema\DateTime::make('createdAt'),
|
||||
|
||||
// Write-only.
|
||||
Schema\Arr::make('users')
|
||||
->requiredOnCreateWithout(['relationships.dialog'])
|
||||
->writableOnCreate()
|
||||
->hidden()
|
||||
->items(1)
|
||||
->set(fn () => null),
|
||||
|
||||
Schema\Relationship\ToOne::make('user')
|
||||
->type('users')
|
||||
->includable(),
|
||||
Schema\Relationship\ToOne::make('dialog')
|
||||
->type('dialogs')
|
||||
->includable()
|
||||
->writableOnCreate()
|
||||
->requiredOnCreateWithout(['attributes.users']),
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
public function sorts(): array
|
||||
{
|
||||
return [
|
||||
SortColumn::make('createdAt'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function creating(object $model, OriginalContext $context): ?object
|
||||
{
|
||||
$model->user_id = $context->getActor()->id;
|
||||
$data = $context->body()['data'] ?? [];
|
||||
|
||||
$this->events->dispatch(
|
||||
new DialogMessage\Event\Creating($model, $data)
|
||||
);
|
||||
|
||||
if (! $model->dialog_id) {
|
||||
$context->getActor()->assertCan('sendAnyMessage');
|
||||
|
||||
$users = array_filter(Arr::pluck($data['attributes']['users'] ?? [], 'id'), fn (mixed $id) => $id && $id != $model->user_id);
|
||||
|
||||
if (empty($users)) {
|
||||
throw new ValidationException([
|
||||
'users' => str_replace(':attribute', 'users', $this->translator->trans('validation.required')),
|
||||
]);
|
||||
}
|
||||
|
||||
$dialog = Dialog::for($model, $users);
|
||||
|
||||
$model->dialog()->associate($dialog);
|
||||
|
||||
$users[] = $model->user_id;
|
||||
|
||||
$dialog->users()->syncWithPivotValues(array_unique($users), [
|
||||
'joined_at' => Carbon::now(),
|
||||
]);
|
||||
}
|
||||
|
||||
return parent::creating($model, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function created(object $model, OriginalContext $context): ?object
|
||||
{
|
||||
if ($model->dialog->last_message_id !== $model->id) {
|
||||
$model->dialog->setLastMessage($model);
|
||||
}
|
||||
|
||||
if (! $model->dialog->first_message_id) {
|
||||
$model->dialog->setFirstMessage($model);
|
||||
}
|
||||
|
||||
$model->dialog->isDirty() && $model->dialog->save();
|
||||
|
||||
$this->bus->dispatch(
|
||||
new ReadDialog($model->dialog_id, $context->getActor(), $model->id)
|
||||
);
|
||||
|
||||
$this->events->dispatch(
|
||||
new DialogMessage\Event\Created($model)
|
||||
);
|
||||
|
||||
return parent::created($model, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function updating(object $model, OriginalContext $context): ?object
|
||||
{
|
||||
$this->events->dispatch(
|
||||
new DialogMessage\Event\Updating($model, $context->body()['data'] ?? [])
|
||||
);
|
||||
|
||||
return parent::updating($model, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function updated(object $model, OriginalContext $context): ?object
|
||||
{
|
||||
$this->events->dispatch(
|
||||
new DialogMessage\Event\Updated($model)
|
||||
);
|
||||
|
||||
return parent::updated($model, $context);
|
||||
}
|
||||
}
|
168
extensions/messages/src/Api/Resource/DialogResource.php
Normal file
168
extensions/messages/src/Api/Resource/DialogResource.php
Normal file
|
@ -0,0 +1,168 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Api\Resource;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Api\Sort\SortColumn;
|
||||
use Flarum\Bus\Dispatcher;
|
||||
use Flarum\Locale\Translator;
|
||||
use Flarum\Messages\Command\ReadDialog;
|
||||
use Flarum\Messages\Dialog;
|
||||
use Flarum\Messages\UserDialogState;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Arr;
|
||||
use Laminas\Diactoros\Response\EmptyResponse;
|
||||
use Tobyz\JsonApiServer\Context as OriginalContext;
|
||||
|
||||
/**
|
||||
* @extends Resource\AbstractDatabaseResource<Dialog>
|
||||
*/
|
||||
class DialogResource extends Resource\AbstractDatabaseResource
|
||||
{
|
||||
public function __construct(
|
||||
protected Translator $translator,
|
||||
protected Dispatcher $bus,
|
||||
) {
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'dialogs';
|
||||
}
|
||||
|
||||
public function model(): string
|
||||
{
|
||||
return Dialog::class;
|
||||
}
|
||||
|
||||
public function scope(Builder $query, OriginalContext $context): void
|
||||
{
|
||||
$query->whereVisibleTo($context->getActor());
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [
|
||||
Endpoint\Show::make()
|
||||
->authenticated()
|
||||
->eagerLoad('state'),
|
||||
Endpoint\Update::make()
|
||||
->authenticated()
|
||||
->eagerLoad('state'),
|
||||
Endpoint\Endpoint::make('read')
|
||||
->route('POST', '/read')
|
||||
->authenticated()
|
||||
->action(function (Context $context) {
|
||||
$connection = UserDialogState::query()->getConnection();
|
||||
$grammar = UserDialogState::query()->getGrammar();
|
||||
|
||||
$table = $grammar->wrapTable('dialogs');
|
||||
$column = $grammar->wrap('last_message_id');
|
||||
|
||||
UserDialogState::query()
|
||||
->where('dialog_user.user_id', $context->getActor()->id)
|
||||
->update([
|
||||
'last_read_message_id' => $connection->raw('('.$grammar->compileSelect(
|
||||
Dialog::query()
|
||||
->select('last_message_id')
|
||||
->from('dialogs')
|
||||
->whereColumn('dialogs.id', 'dialog_user.dialog_id')
|
||||
->toBase()
|
||||
).')'),
|
||||
'last_read_at' => Carbon::now(),
|
||||
]);
|
||||
})
|
||||
->response(fn () => new EmptyResponse(204)),
|
||||
Endpoint\Index::make()
|
||||
->authenticated()
|
||||
->paginate()
|
||||
->eagerLoad(['users', 'state']),
|
||||
];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
return [
|
||||
|
||||
Schema\Str::make('title')
|
||||
->get(function (Dialog $dialog, Context $context) {
|
||||
return $this->translator->trans('flarum-messages.lib.dialog.title', [
|
||||
'{username}' => $dialog->recipient($context->getActor())->display_name,
|
||||
]);
|
||||
}),
|
||||
Schema\Str::make('type')
|
||||
->minLength(3)
|
||||
->maxLength(255)
|
||||
->in(Dialog::$types),
|
||||
Schema\DateTime::make('lastMessageAt'),
|
||||
Schema\DateTime::make('createdAt'),
|
||||
|
||||
Schema\Integer::make('unreadCount')
|
||||
->countRelation('messages', function (Builder $query, Context $context) {
|
||||
$query->leftJoin('dialog_user', 'dialog_messages.dialog_id', '=', 'dialog_user.dialog_id')
|
||||
->where('dialog_user.user_id', $context->getActor()->id)
|
||||
->whereColumn('dialog_messages.id', '>', 'dialog_user.last_read_message_id')
|
||||
->groupBy('dialog_messages.dialog_id');
|
||||
}),
|
||||
Schema\DateTime::make('lastReadAt')
|
||||
->visible(fn (Dialog $dialog) => $dialog->state !== null)
|
||||
->get(function (Dialog $dialog) {
|
||||
return $dialog->state->last_read_at;
|
||||
}),
|
||||
Schema\Integer::make('lastReadMessageId')
|
||||
->visible(fn (Dialog $dialog) => $dialog->state !== null)
|
||||
->get(function (Dialog $dialog) {
|
||||
return $dialog->state?->last_read_message_id;
|
||||
})
|
||||
->writableOnUpdate()
|
||||
->set(function (Dialog $dialog, int $value, Context $context) {
|
||||
if ($readNumber = Arr::get($context->body(), 'data.attributes.lastReadMessageId')) {
|
||||
$dialog->afterSave(function (Dialog $dialog) use ($readNumber, $context) {
|
||||
$this->bus->dispatch(
|
||||
new ReadDialog($dialog->id, $context->getActor(), $readNumber)
|
||||
);
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
Schema\Relationship\ToMany::make('messages')
|
||||
->type('dialog-messages'),
|
||||
Schema\Relationship\ToMany::make('users')
|
||||
->type('users')
|
||||
->scope(fn (BelongsToMany $query) => $query->limit(5))
|
||||
->includable(),
|
||||
Schema\Relationship\ToOne::make('firstMessage')
|
||||
->type('dialog-messages')
|
||||
->includable(),
|
||||
Schema\Relationship\ToOne::make('lastMessage')
|
||||
->type('dialog-messages')
|
||||
->includable(),
|
||||
Schema\Relationship\ToOne::make('lastMessageUser')
|
||||
->type('users')
|
||||
->includable(),
|
||||
];
|
||||
}
|
||||
|
||||
public function sorts(): array
|
||||
{
|
||||
return [
|
||||
SortColumn::make('createdAt')
|
||||
->ascendingAlias('oldest')
|
||||
->descendingAlias('newest'),
|
||||
SortColumn::make('lastMessageAt')
|
||||
->descendingAlias('latest'),
|
||||
];
|
||||
}
|
||||
}
|
22
extensions/messages/src/Command/ReadDialog.php
Normal file
22
extensions/messages/src/Command/ReadDialog.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Command;
|
||||
|
||||
use Flarum\User\User;
|
||||
|
||||
class ReadDialog
|
||||
{
|
||||
public function __construct(
|
||||
public int $dialogId,
|
||||
public User $actor,
|
||||
public int $lastReadMessageId
|
||||
) {
|
||||
}
|
||||
}
|
50
extensions/messages/src/Command/ReadDialogHandler.php
Normal file
50
extensions/messages/src/Command/ReadDialogHandler.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Command;
|
||||
|
||||
use Flarum\Foundation\DispatchEventsTrait;
|
||||
use Flarum\Messages\Dialog;
|
||||
use Flarum\Messages\Dialog\Event\UserDataSaving;
|
||||
use Flarum\Messages\UserDialogState;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
class ReadDialogHandler
|
||||
{
|
||||
use DispatchEventsTrait;
|
||||
|
||||
public function __construct(
|
||||
protected Dispatcher $events,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(ReadDialog $command): UserDialogState
|
||||
{
|
||||
$actor = $command->actor;
|
||||
|
||||
$actor->assertRegistered();
|
||||
|
||||
/** @var Dialog $dialog */
|
||||
$dialog = Dialog::whereVisibleTo($actor)->findOrFail($command->dialogId);
|
||||
|
||||
/** @var UserDialogState $state */
|
||||
$state = $dialog->state($actor)->first();
|
||||
$state->read($command->lastReadMessageId);
|
||||
|
||||
$this->events->dispatch(
|
||||
new UserDataSaving($state)
|
||||
);
|
||||
|
||||
$state->save();
|
||||
|
||||
$this->dispatchEventsFor($state);
|
||||
|
||||
return $state;
|
||||
}
|
||||
}
|
127
extensions/messages/src/Dialog.php
Normal file
127
extensions/messages/src/Dialog.php
Normal file
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Database\ScopeVisibilityTrait;
|
||||
use Flarum\Foundation\EventGeneratorTrait;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int|null $first_message_id
|
||||
* @property int|null $last_message_id
|
||||
* @property \Carbon\Carbon|null $last_message_at
|
||||
* @property int|null $last_message_user_id
|
||||
* @property string $type
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, DialogMessage> $messages
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, User> $users
|
||||
* @property-read DialogMessage|null $firstMessage
|
||||
* @property-read DialogMessage|null $lastMessage
|
||||
* @property-read User|null $lastMessageUser
|
||||
* @property-read UserDialogState|null $state
|
||||
*/
|
||||
class Dialog extends AbstractModel
|
||||
{
|
||||
use EventGeneratorTrait;
|
||||
use ScopeVisibilityTrait;
|
||||
|
||||
protected $table = 'dialogs';
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
public static array $types = ['direct'];
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected static ?User $stateUser = null;
|
||||
|
||||
public function messages(): HasMany
|
||||
{
|
||||
return $this->hasMany(DialogMessage::class);
|
||||
}
|
||||
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'dialog_user');
|
||||
}
|
||||
|
||||
public function firstMessage(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DialogMessage::class, 'first_message_id');
|
||||
}
|
||||
|
||||
public function lastMessage(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DialogMessage::class, 'last_message_id');
|
||||
}
|
||||
|
||||
public function lastMessageUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'last_message_user_id');
|
||||
}
|
||||
|
||||
public function state(?User $user = null): HasOne
|
||||
{
|
||||
$user = $user ?: static::$stateUser;
|
||||
|
||||
return $this->hasOne(UserDialogState::class)->where('user_id', $user?->id);
|
||||
}
|
||||
|
||||
public function setFirstMessage(DialogMessage $message): static
|
||||
{
|
||||
$this->created_at = $message->created_at;
|
||||
$this->first_message_id = $message->id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLastMessage(DialogMessage $message): static
|
||||
{
|
||||
$this->last_message_at = $message->created_at;
|
||||
$this->last_message_user_id = $message->user_id;
|
||||
$this->last_message_id = $message->id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public static function for(DialogMessage $model, array $users): self
|
||||
{
|
||||
$otherUserId = array_values(array_diff($users, [$model->user_id]))[0] ?? null;
|
||||
|
||||
if (! $otherUserId) {
|
||||
throw new InvalidArgumentException('Dialog must have at least two users');
|
||||
}
|
||||
|
||||
return self::query()
|
||||
->whereRelation('users', 'user_id', $model->user_id)
|
||||
->whereRelation('users', 'user_id', $otherUserId)
|
||||
->firstOrCreate([
|
||||
'type' => 'direct',
|
||||
]);
|
||||
}
|
||||
|
||||
public function recipient(?User $actor): ?User
|
||||
{
|
||||
return $this->users->first(fn (User $user) => $user->id !== $actor?->id);
|
||||
}
|
||||
|
||||
public static function setStateUser(User $user): void
|
||||
{
|
||||
static::$stateUser = $user;
|
||||
}
|
||||
}
|
20
extensions/messages/src/Dialog/Event/UserDataSaving.php
Normal file
20
extensions/messages/src/Dialog/Event/UserDataSaving.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Dialog\Event;
|
||||
|
||||
use Flarum\Messages\UserDialogState;
|
||||
|
||||
class UserDataSaving
|
||||
{
|
||||
public function __construct(
|
||||
public UserDialogState $state
|
||||
) {
|
||||
}
|
||||
}
|
20
extensions/messages/src/Dialog/Event/UserRead.php
Normal file
20
extensions/messages/src/Dialog/Event/UserRead.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Dialog\Event;
|
||||
|
||||
use Flarum\Messages\UserDialogState;
|
||||
|
||||
class UserRead
|
||||
{
|
||||
public function __construct(
|
||||
public UserDialogState $state
|
||||
) {
|
||||
}
|
||||
}
|
35
extensions/messages/src/Dialog/Filter/UnreadFilter.php
Normal file
35
extensions/messages/src/Dialog/Filter/UnreadFilter.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Dialog\Filter;
|
||||
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class UnreadFilter implements FilterInterface
|
||||
{
|
||||
public function getFilterKey(): string
|
||||
{
|
||||
return 'unread';
|
||||
}
|
||||
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
{
|
||||
$state->getQuery()->whereHas('users', function (Builder $query) use ($state) {
|
||||
$query
|
||||
->where('dialog_user.user_id', $state->getActor()->id)
|
||||
->whereColumn('dialog_user.last_read_message_id', '<', 'dialogs.last_message_id');
|
||||
});
|
||||
}
|
||||
}
|
51
extensions/messages/src/DialogMessage.php
Normal file
51
extensions/messages/src/DialogMessage.php
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Database\ScopeVisibilityTrait;
|
||||
use Flarum\Formatter\Formattable;
|
||||
use Flarum\Formatter\HasFormattedContent;
|
||||
use Flarum\Foundation\EventGeneratorTrait;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $dialog_id
|
||||
* @property int|null $user_id
|
||||
* @property string $content
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
* @property-read Dialog $dialog
|
||||
* @property-read User|null $user
|
||||
*/
|
||||
class DialogMessage extends AbstractModel implements Formattable
|
||||
{
|
||||
use EventGeneratorTrait;
|
||||
use ScopeVisibilityTrait;
|
||||
use HasFormattedContent;
|
||||
|
||||
protected $table = 'dialog_messages';
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function dialog(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Dialog::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
20
extensions/messages/src/DialogMessage/Event/Created.php
Normal file
20
extensions/messages/src/DialogMessage/Event/Created.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\DialogMessage\Event;
|
||||
|
||||
use Flarum\Messages\DialogMessage;
|
||||
|
||||
class Created
|
||||
{
|
||||
public function __construct(
|
||||
public DialogMessage $message
|
||||
) {
|
||||
}
|
||||
}
|
21
extensions/messages/src/DialogMessage/Event/Creating.php
Normal file
21
extensions/messages/src/DialogMessage/Event/Creating.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\DialogMessage\Event;
|
||||
|
||||
use Flarum\Messages\DialogMessage;
|
||||
|
||||
class Creating
|
||||
{
|
||||
public function __construct(
|
||||
protected DialogMessage $message,
|
||||
protected array $data
|
||||
) {
|
||||
}
|
||||
}
|
20
extensions/messages/src/DialogMessage/Event/Updated.php
Normal file
20
extensions/messages/src/DialogMessage/Event/Updated.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\DialogMessage\Event;
|
||||
|
||||
use Flarum\Messages\DialogMessage;
|
||||
|
||||
class Updated
|
||||
{
|
||||
public function __construct(
|
||||
protected DialogMessage $message
|
||||
) {
|
||||
}
|
||||
}
|
21
extensions/messages/src/DialogMessage/Event/Updating.php
Normal file
21
extensions/messages/src/DialogMessage/Event/Updating.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\DialogMessage\Event;
|
||||
|
||||
use Flarum\Messages\DialogMessage;
|
||||
|
||||
class Updating
|
||||
{
|
||||
public function __construct(
|
||||
protected DialogMessage $message,
|
||||
protected array $data
|
||||
) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\DialogMessage\Filter;
|
||||
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
*/
|
||||
class DialogFilter implements FilterInterface
|
||||
{
|
||||
public function getFilterKey(): string
|
||||
{
|
||||
return 'dialog';
|
||||
}
|
||||
|
||||
public function filter(SearchState $state, string|array $value, bool $negate): void
|
||||
{
|
||||
$state->getQuery()->where('dialog_id', $value, $negate ? '!=' : '=');
|
||||
}
|
||||
}
|
26
extensions/messages/src/DialogServiceProvider.php
Normal file
26
extensions/messages/src/DialogServiceProvider.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages;
|
||||
|
||||
use Flarum\Formatter\Formatter;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
|
||||
class DialogServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function boot(Formatter $formatter): void
|
||||
{
|
||||
DialogMessage::setFormatter($formatter);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Http\Middleware;
|
||||
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Messages\Dialog;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
class PopulateDialogWithActor implements MiddlewareInterface
|
||||
{
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
Dialog::setStateUser($actor);
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
40
extensions/messages/src/Job/SendMessageNotificationsJob.php
Normal file
40
extensions/messages/src/Job/SendMessageNotificationsJob.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Job;
|
||||
|
||||
use Flarum\Messages\DialogMessage;
|
||||
use Flarum\Messages\Notification\MessageReceivedBlueprint;
|
||||
use Flarum\Notification\NotificationSyncer;
|
||||
use Flarum\Queue\AbstractJob;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
|
||||
class SendMessageNotificationsJob extends AbstractJob
|
||||
{
|
||||
public function __construct(
|
||||
protected DialogMessage $message
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(NotificationSyncer $notifications): void
|
||||
{
|
||||
$users = User::query()
|
||||
->whereIn('id', function (Builder $query) {
|
||||
$query->select('dialog_user.user_id')
|
||||
->from('dialog_user')
|
||||
->where('dialog_user.dialog_id', $this->message->dialog_id);
|
||||
})
|
||||
->where('id', '!=', $this->message->user_id)
|
||||
->get()
|
||||
->all();
|
||||
|
||||
$notifications->sync(new MessageReceivedBlueprint($this->message), $users);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Listener;
|
||||
|
||||
use Flarum\Messages\DialogMessage;
|
||||
use Flarum\Messages\Job;
|
||||
use Illuminate\Contracts\Queue\Queue;
|
||||
|
||||
class SendNotificationWhenMessageSent
|
||||
{
|
||||
public function __construct(
|
||||
protected Queue $queue
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(DialogMessage\Event\Created $event): void
|
||||
{
|
||||
$this->queue->push(new Job\SendMessageNotificationsJob($event->message));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Notification;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Locale\TranslatorInterface;
|
||||
use Flarum\Messages\DialogMessage;
|
||||
use Flarum\Notification\Blueprint\BlueprintInterface;
|
||||
use Flarum\Notification\MailableInterface;
|
||||
use Flarum\User\User;
|
||||
|
||||
class MessageReceivedBlueprint implements BlueprintInterface, MailableInterface
|
||||
{
|
||||
public function __construct(
|
||||
public DialogMessage $message
|
||||
) {
|
||||
}
|
||||
|
||||
public function getFromUser(): ?User
|
||||
{
|
||||
return $this->message->user;
|
||||
}
|
||||
|
||||
public function getSubject(): ?AbstractModel
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function getData(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getEmailViews(): array
|
||||
{
|
||||
return [
|
||||
'text' => 'flarum-messages::emails.plain.messageReceived',
|
||||
'html' => 'flarum-messages::emails.html.messageReceived'
|
||||
];
|
||||
}
|
||||
|
||||
public function getEmailSubject(TranslatorInterface $translator): string
|
||||
{
|
||||
return $translator->trans('flarum-messages.email.message_received.subject', [
|
||||
'{user_display_name}' => $this->message->user->display_name,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getType(): string
|
||||
{
|
||||
return 'messageReceived';
|
||||
}
|
||||
|
||||
public static function getSubjectModel(): string
|
||||
{
|
||||
return DialogMessage::class;
|
||||
}
|
||||
}
|
23
extensions/messages/src/Search/DialogMessageSearcher.php
Normal file
23
extensions/messages/src/Search/DialogMessageSearcher.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Search;
|
||||
|
||||
use Flarum\Messages\DialogMessage;
|
||||
use Flarum\Search\Database\AbstractSearcher;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class DialogMessageSearcher extends AbstractSearcher
|
||||
{
|
||||
public function getQuery(User $actor): Builder
|
||||
{
|
||||
return DialogMessage::whereVisibleTo($actor);
|
||||
}
|
||||
}
|
23
extensions/messages/src/Search/DialogSearcher.php
Normal file
23
extensions/messages/src/Search/DialogSearcher.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Search;
|
||||
|
||||
use Flarum\Messages\Dialog;
|
||||
use Flarum\Search\Database\AbstractSearcher;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class DialogSearcher extends AbstractSearcher
|
||||
{
|
||||
public function getQuery(User $actor): Builder
|
||||
{
|
||||
return Dialog::whereVisibleTo($actor);
|
||||
}
|
||||
}
|
63
extensions/messages/src/UserDialogState.php
Normal file
63
extensions/messages/src/UserDialogState.php
Normal file
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Foundation\EventGeneratorTrait;
|
||||
use Flarum\Messages\Dialog\Event\UserRead;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $dialog_id
|
||||
* @property int $user_id
|
||||
* @property \Carbon\Carbon $joined_at
|
||||
* @property int|null $last_read_message_id
|
||||
* @property \Carbon\Carbon $last_read_at
|
||||
* @property-read Dialog $dialog
|
||||
* @property-read User $user
|
||||
*/
|
||||
class UserDialogState extends AbstractModel
|
||||
{
|
||||
use EventGeneratorTrait;
|
||||
|
||||
protected $table = 'dialog_user';
|
||||
|
||||
protected $casts = [
|
||||
'user_id' => 'integer',
|
||||
'dialog_id' => 'integer',
|
||||
'joined_at' => 'datetime',
|
||||
'last_read_message_id' => 'integer'
|
||||
];
|
||||
|
||||
public function dialog(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Dialog::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function read(int $messageId): static
|
||||
{
|
||||
if ($messageId > $this->last_read_message_id) {
|
||||
$this->last_read_message_id = $messageId;
|
||||
$this->last_read_at = Carbon::now();
|
||||
|
||||
$this->raise(new UserRead($this));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
0
extensions/messages/tests/fixtures/.gitkeep
vendored
Normal file
0
extensions/messages/tests/fixtures/.gitkeep
vendored
Normal file
128
extensions/messages/tests/integration/api/ListTest.php
Normal file
128
extensions/messages/tests/integration/api/ListTest.php
Normal file
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Messages\Dialog;
|
||||
use Flarum\Messages\DialogMessage;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
class ListTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->extension('flarum-messages');
|
||||
|
||||
$this->prepareDatabase([
|
||||
User::class => [
|
||||
['id' => 3, 'username' => 'astarion'],
|
||||
['id' => 4, 'username' => 'gale'],
|
||||
['id' => 5, 'username' => 'karlach'],
|
||||
],
|
||||
Dialog::class => [
|
||||
['id' => 102, 'type' => 'direct'],
|
||||
['id' => 103, 'type' => 'direct'],
|
||||
['id' => 104, 'type' => 'direct'],
|
||||
],
|
||||
DialogMessage::class => [
|
||||
['id' => 102, 'dialog_id' => 102, 'user_id' => 3, 'content' => 'Hello, Gale!'],
|
||||
['id' => 103, 'dialog_id' => 102, 'user_id' => 4, 'content' => 'Hello, Astarion!'],
|
||||
['id' => 104, 'dialog_id' => 103, 'user_id' => 3, 'content' => 'Hello, Karlach!'],
|
||||
['id' => 105, 'dialog_id' => 103, 'user_id' => 5, 'content' => 'Hello, Astarion!'],
|
||||
['id' => 106, 'dialog_id' => 104, 'user_id' => 4, 'content' => 'Hello, Karlach!'],
|
||||
['id' => 107, 'dialog_id' => 104, 'user_id' => 5, 'content' => 'Hello, Gale!'],
|
||||
],
|
||||
'dialog_user' => [
|
||||
['dialog_id' => 102, 'user_id' => 3, 'joined_at' => Carbon::now()],
|
||||
['dialog_id' => 102, 'user_id' => 4, 'joined_at' => Carbon::now()],
|
||||
['dialog_id' => 103, 'user_id' => 3, 'joined_at' => Carbon::now()],
|
||||
['dialog_id' => 103, 'user_id' => 5, 'joined_at' => Carbon::now()],
|
||||
['dialog_id' => 104, 'user_id' => 4, 'joined_at' => Carbon::now()],
|
||||
['dialog_id' => 104, 'user_id' => 5, 'joined_at' => Carbon::now()],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[DataProvider('dialogsAccessProvider')]
|
||||
public function test_can_list_accessible_dialogs(int $actorId, array $visibleDialogs): void
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/dialogs', [
|
||||
'authenticatedAs' => $actorId,
|
||||
])->withQueryParams(['include' => 'users'])
|
||||
);
|
||||
|
||||
$json = $response->getBody()->getContents();
|
||||
$prettyJson = json_encode($json, JSON_PRETTY_PRINT);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $prettyJson);
|
||||
$this->assertJson($json);
|
||||
|
||||
$data = json_decode($json, true)['data'];
|
||||
|
||||
$this->assertCount(count($visibleDialogs), $data);
|
||||
|
||||
foreach ($visibleDialogs as $dialogId) {
|
||||
$ids = array_column($data, 'id');
|
||||
$this->assertContains((string) $dialogId, $ids, json_encode($ids, JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
|
||||
public static function dialogsAccessProvider(): array
|
||||
{
|
||||
return [
|
||||
'Astarion can see dialogs with Gale and Karlach' => [3, [102, 103]],
|
||||
'Gale can see dialogs with Astarion and Karlach' => [4, [102, 104]],
|
||||
'Karlach can see dialogs with Astarion and Gale' => [5, [103, 104]],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('dialogMessagesAccessProvider')]
|
||||
public function test_can_list_accessible_dialog_messages(int $actorId, array $visibleDialogMessages): void
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('GET', '/api/dialog-messages', [
|
||||
'authenticatedAs' => $actorId,
|
||||
])->withQueryParams(['include' => 'dialog']),
|
||||
);
|
||||
|
||||
$json = $response->getBody()->getContents();
|
||||
$prettyJson = json_encode($json, JSON_PRETTY_PRINT);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $prettyJson);
|
||||
$this->assertJson($json);
|
||||
|
||||
$data = json_decode($json, true)['data'];
|
||||
$prettyJson = json_encode(json_decode($json), JSON_PRETTY_PRINT);
|
||||
|
||||
$this->assertCount(count($visibleDialogMessages), $data, $prettyJson);
|
||||
|
||||
foreach ($visibleDialogMessages as $dialogMessageId) {
|
||||
$ids = array_column($data, 'id');
|
||||
$this->assertContains((string) $dialogMessageId, $ids, json_encode($ids, JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
|
||||
public static function dialogMessagesAccessProvider(): array
|
||||
{
|
||||
return [
|
||||
'Astarion can see messages in dialogs with Gale and Karlach' => [3, [102, 103, 104, 105]],
|
||||
'Gale can see messages in dialogs with Astarion and Karlach' => [4, [102, 103, 106, 107]],
|
||||
'Karlach can see messages in dialogs with Astarion and Gale' => [5, [104, 105, 106, 107]],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Tests\integration\api\dialog_messages;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Messages\Dialog;
|
||||
use Flarum\Messages\DialogMessage;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
|
||||
class CreateTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->extension('flarum-messages');
|
||||
|
||||
$this->prepareDatabase([
|
||||
User::class => [
|
||||
['id' => 3, 'username' => 'alice'],
|
||||
['id' => 4, 'username' => 'bob'],
|
||||
['id' => 5, 'username' => 'karlach'],
|
||||
],
|
||||
Dialog::class => [
|
||||
['id' => 102, 'type' => 'direct'],
|
||||
],
|
||||
DialogMessage::class => [
|
||||
['id' => 102, 'dialog_id' => 102, 'user_id' => 4, 'content' => 'Hello, Karlach!'],
|
||||
],
|
||||
'dialog_user' => [
|
||||
['dialog_id' => 102, 'user_id' => 4, 'joined_at' => Carbon::now()],
|
||||
['dialog_id' => 102, 'user_id' => 5, 'joined_at' => Carbon::now()],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_can_create_a_direct_private_conversation_with_someone(): void
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/dialog-messages', [
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'dialog-messages',
|
||||
'attributes' => [
|
||||
'content' => 'Hello, Bob!',
|
||||
'users' => [
|
||||
['id' => 4],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
])->withQueryParams(['include' => 'dialog.users,user'])
|
||||
);
|
||||
|
||||
$json = $response->getBody()->getContents();
|
||||
$data = json_decode($json, true);
|
||||
$pretty = json_encode($data, JSON_PRETTY_PRINT);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode(), $pretty);
|
||||
$this->assertNotEquals(102, $data['data']['relationships']['dialog']['data']['id'], $pretty);
|
||||
$this->assertEquals('direct', collect($data['included'])->firstWhere('type', 'dialogs')['attributes']['type'], $pretty);
|
||||
$this->assertEquals('Hello, Bob!', $data['data']['attributes']['contentHtml'], $pretty);
|
||||
$this->assertEqualsCanonicalizing([3, 4], collect(collect($data['included'])->firstWhere('type', 'dialogs')['relationships']['users']['data'])->pluck('id')->all(), $pretty);
|
||||
}
|
||||
|
||||
public function test_can_create_a_private_message_when_conversation_already_exists(): void
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/dialog-messages', [
|
||||
'authenticatedAs' => 5,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'dialog-messages',
|
||||
'attributes' => [
|
||||
'content' => 'Hello, Bob!',
|
||||
],
|
||||
'relationships' => [
|
||||
'dialog' => [
|
||||
'data' => [
|
||||
'type' => 'dialogs',
|
||||
'id' => '102',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
])->withQueryParams(['include' => 'dialog.users,user'])
|
||||
);
|
||||
|
||||
$json = $response->getBody()->getContents();
|
||||
$data = json_decode($json, true);
|
||||
$pretty = json_encode($data, JSON_PRETTY_PRINT);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode(), $pretty);
|
||||
$this->assertEquals(102, $data['data']['relationships']['dialog']['data']['id'], $pretty);
|
||||
$this->assertEquals('direct', collect($data['included'])->firstWhere('type', 'dialogs')['attributes']['type'], $pretty);
|
||||
$this->assertEquals('Hello, Bob!', $data['data']['attributes']['contentHtml'], $pretty);
|
||||
$this->assertEqualsCanonicalizing([4, 5], collect(collect($data['included'])->firstWhere('type', 'dialogs')['relationships']['users']['data'])->pluck('id')->all(), $pretty);
|
||||
}
|
||||
}
|
112
extensions/messages/tests/integration/api/dialogs/UpdateTest.php
Normal file
112
extensions/messages/tests/integration/api/dialogs/UpdateTest.php
Normal file
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Messages\Tests\integration\api\dialogs;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Messages\Dialog;
|
||||
use Flarum\Messages\DialogMessage;
|
||||
use Flarum\Messages\UserDialogState;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
|
||||
class UpdateTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->extension('flarum-messages');
|
||||
|
||||
$this->prepareDatabase([
|
||||
User::class => [
|
||||
['id' => 3, 'username' => 'alice'],
|
||||
['id' => 4, 'username' => 'bob'],
|
||||
['id' => 5, 'username' => 'karlach'],
|
||||
],
|
||||
Dialog::class => [
|
||||
['id' => 102, 'type' => 'direct', 'last_message_id' => 111],
|
||||
],
|
||||
DialogMessage::class => [
|
||||
['id' => 102, 'dialog_id' => 102, 'user_id' => 4, 'content' => '<p>Hello, Alice!</p>'],
|
||||
['id' => 103, 'dialog_id' => 102, 'user_id' => 3, 'content' => '<p>Hello, Bob!</p>'],
|
||||
['id' => 104, 'dialog_id' => 102, 'user_id' => 4, 'content' => '<p>Hello, Alice!</p>'],
|
||||
['id' => 105, 'dialog_id' => 102, 'user_id' => 3, 'content' => '<p>Hello, Bob!</p>'],
|
||||
['id' => 106, 'dialog_id' => 102, 'user_id' => 4, 'content' => '<p>Hello, Alice!</p>'],
|
||||
['id' => 107, 'dialog_id' => 102, 'user_id' => 3, 'content' => '<p>Hello, Bob!</p>'],
|
||||
['id' => 108, 'dialog_id' => 102, 'user_id' => 4, 'content' => '<p>Hello, Alice!</p>'],
|
||||
['id' => 109, 'dialog_id' => 102, 'user_id' => 3, 'content' => '<p>Hello, Bob!</p>'],
|
||||
['id' => 110, 'dialog_id' => 102, 'user_id' => 4, 'content' => '<p>Hello, Alice!</p>'],
|
||||
['id' => 111, 'dialog_id' => 102, 'user_id' => 3, 'content' => '<p>Hello, Bob!</p>'],
|
||||
],
|
||||
'dialog_user' => [
|
||||
['dialog_id' => 102, 'user_id' => 3, 'last_read_message_id' => 0, 'last_read_at' => null, 'joined_at' => Carbon::now()],
|
||||
['dialog_id' => 102, 'user_id' => 4, 'last_read_message_id' => 0, 'last_read_at' => null, 'joined_at' => Carbon::now()],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_can_mark_dialog_as_read(): void
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/dialogs/102', [
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'type' => 'dialogs',
|
||||
'id' => '102',
|
||||
'attributes' => [
|
||||
'lastReadMessageId' => 107,
|
||||
],
|
||||
],
|
||||
],
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
|
||||
$state = UserDialogState::query()
|
||||
->where('dialog_id', 102)
|
||||
->where('user_id', 3)
|
||||
->first();
|
||||
|
||||
$this->assertEquals(107, $state->last_read_message_id);
|
||||
$this->assertNotNull($state->last_read_at);
|
||||
}
|
||||
|
||||
public function test_can_mark_all_as_read(): void
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/dialogs/read', [
|
||||
'authenticatedAs' => 3,
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(204, $response->getStatusCode(), json_encode(json_decode($response->getBody()->getContents()), JSON_PRETTY_PRINT));
|
||||
|
||||
$state = UserDialogState::query()
|
||||
->where('dialog_id', 102)
|
||||
->where('user_id', 3)
|
||||
->first();
|
||||
|
||||
$nonState = UserDialogState::query()
|
||||
->where('dialog_id', 102)
|
||||
->where('user_id', '!=', 3)
|
||||
->first();
|
||||
|
||||
$this->assertNotNull($state->last_read_at);
|
||||
$this->assertNull($nonState->last_read_at);
|
||||
|
||||
$this->assertEquals(111, $state->last_read_message_id);
|
||||
$this->assertEquals(0, $nonState->last_read_message_id);
|
||||
}
|
||||
}
|
12
extensions/messages/tests/integration/setup.php
Normal file
12
extensions/messages/tests/integration/setup.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
$setup = require __DIR__.'/../../../../php-packages/testing/bootstrap/monorepo.php';
|
||||
|
||||
$setup->run();
|
24
extensions/messages/tests/phpunit.integration.xml
Normal file
24
extensions/messages/tests/phpunit.integration.xml
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
backupStaticProperties="false"
|
||||
colors="true"
|
||||
processIsolation="true"
|
||||
stopOnFailure="false"
|
||||
bootstrap="../../../php-packages/testing/bootstrap/monorepo.php"
|
||||
>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">../src/</directory>
|
||||
</include>
|
||||
</source>
|
||||
<testsuites>
|
||||
<testsuite name="Flarum Integration Tests">
|
||||
<directory suffix="Test.php">./integration</directory>
|
||||
<exclude>./integration/tmp</exclude>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user