Skip to content

Commit 114fa80

Browse files
committed
Merge branch 'development' into release
2 parents 8fcd3b2 + 38d3697 commit 114fa80

File tree

286 files changed

+4421
-1446
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

286 files changed

+4421
-1446
lines changed

.github/translators.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,3 +512,10 @@ David Olsen (dawin) :: Danish
512512
ltnzr :: French
513513
Frank Holler (holler.frank) :: German; German Informal
514514
Korab Arifi (korabidev) :: Albanian
515+
Petr Husák (petrhusak) :: Czech
516+
Bernardo Maia (bernardo.bmaia2) :: Portuguese, Brazilian
517+
Amr (amr3k) :: Arabic
518+
Tahsin Ahmed (tahsinahmed2012) :: Bengali
519+
bojan_che :: Serbian (Cyrillic)
520+
setiawan setiawan (culture.setiawan) :: Indonesian
521+
Donald Mac Kenzie (kiuman) :: Norwegian Bokmal

app/Access/Mfa/TotpService.php

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@
1414

1515
class TotpService
1616
{
17-
protected $google2fa;
18-
19-
public function __construct(Google2FA $google2fa)
20-
{
17+
public function __construct(
18+
protected Google2FA $google2fa
19+
) {
2120
$this->google2fa = $google2fa;
2221
// Use SHA1 as a default, Personal testing of other options in 2021 found
2322
// many apps lack support for other algorithms yet still will scan
@@ -35,7 +34,7 @@ public function generateSecret(): string
3534
}
3635

3736
/**
38-
* Generate a TOTP URL from secret key.
37+
* Generate a TOTP URL from a secret key.
3938
*/
4039
public function generateUrl(string $secret, User $user): string
4140
{
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace BookStack\Activity\Models;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Support\Carbon;
7+
8+
/**
9+
* @property int $id
10+
* @property string $mentionable_type
11+
* @property int $mentionable_id
12+
* @property int $from_user_id
13+
* @property int $to_user_id
14+
* @property Carbon $created_at
15+
* @property Carbon $updated_at
16+
*/
17+
class MentionHistory extends Model
18+
{
19+
protected $table = 'mention_history';
20+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
namespace BookStack\Activity\Notifications\Handlers;
4+
5+
use BookStack\Activity\ActivityType;
6+
use BookStack\Activity\Models\Activity;
7+
use BookStack\Activity\Models\Comment;
8+
use BookStack\Activity\Models\Loggable;
9+
use BookStack\Activity\Models\MentionHistory;
10+
use BookStack\Activity\Notifications\Messages\CommentMentionNotification;
11+
use BookStack\Activity\Tools\MentionParser;
12+
use BookStack\Entities\Models\Page;
13+
use BookStack\Settings\UserNotificationPreferences;
14+
use BookStack\Users\Models\User;
15+
use Illuminate\Database\Eloquent\Collection;
16+
use Illuminate\Support\Carbon;
17+
18+
class CommentMentionNotificationHandler extends BaseNotificationHandler
19+
{
20+
public function handle(Activity $activity, Loggable|string $detail, User $user): void
21+
{
22+
if (!($detail instanceof Comment) || !($detail->entity instanceof Page)) {
23+
throw new \InvalidArgumentException("Detail for comment mention notifications must be a comment on a page");
24+
}
25+
26+
/** @var Page $page */
27+
$page = $detail->entity;
28+
29+
$parser = new MentionParser();
30+
$mentionedUserIds = $parser->parseUserIdsFromHtml($detail->html);
31+
$realMentionedUsers = User::whereIn('id', $mentionedUserIds)->get();
32+
33+
$receivingNotifications = $realMentionedUsers->filter(function (User $user) {
34+
$prefs = new UserNotificationPreferences($user);
35+
return $prefs->notifyOnCommentMentions();
36+
});
37+
$receivingNotificationsUserIds = $receivingNotifications->pluck('id')->toArray();
38+
39+
$userMentionsToLog = $realMentionedUsers;
40+
41+
// When an edit, we check our history to see if we've already notified the user about this comment before
42+
// so that we can filter them out to avoid double notifications.
43+
if ($activity->type === ActivityType::COMMENT_UPDATE) {
44+
$previouslyNotifiedUserIds = $this->getPreviouslyNotifiedUserIds($detail);
45+
$receivingNotificationsUserIds = array_values(array_diff($receivingNotificationsUserIds, $previouslyNotifiedUserIds));
46+
$userMentionsToLog = $userMentionsToLog->filter(function (User $user) use ($previouslyNotifiedUserIds) {
47+
return !in_array($user->id, $previouslyNotifiedUserIds);
48+
});
49+
}
50+
51+
$this->logMentions($userMentionsToLog, $detail, $user);
52+
$this->sendNotificationToUserIds(CommentMentionNotification::class, $receivingNotificationsUserIds, $user, $detail, $page);
53+
}
54+
55+
/**
56+
* @param Collection<User> $mentionedUsers
57+
*/
58+
protected function logMentions(Collection $mentionedUsers, Comment $comment, User $fromUser): void
59+
{
60+
$mentions = [];
61+
$now = Carbon::now();
62+
63+
foreach ($mentionedUsers as $mentionedUser) {
64+
$mentions[] = [
65+
'mentionable_type' => $comment->getMorphClass(),
66+
'mentionable_id' => $comment->id,
67+
'from_user_id' => $fromUser->id,
68+
'to_user_id' => $mentionedUser->id,
69+
'created_at' => $now,
70+
'updated_at' => $now,
71+
];
72+
}
73+
74+
MentionHistory::query()->insert($mentions);
75+
}
76+
77+
protected function getPreviouslyNotifiedUserIds(Comment $comment): array
78+
{
79+
return MentionHistory::query()
80+
->where('mentionable_id', $comment->id)
81+
->where('mentionable_type', $comment->getMorphClass())
82+
->pluck('to_user_id')
83+
->toArray();
84+
}
85+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace BookStack\Activity\Notifications\Messages;
4+
5+
use BookStack\Activity\Models\Comment;
6+
use BookStack\Activity\Notifications\MessageParts\EntityLinkMessageLine;
7+
use BookStack\Activity\Notifications\MessageParts\ListMessageLine;
8+
use BookStack\Entities\Models\Page;
9+
use BookStack\Users\Models\User;
10+
use Illuminate\Notifications\Messages\MailMessage;
11+
12+
class CommentMentionNotification extends BaseActivityNotification
13+
{
14+
public function toMail(User $notifiable): MailMessage
15+
{
16+
/** @var Comment $comment */
17+
$comment = $this->detail;
18+
/** @var Page $page */
19+
$page = $comment->entity;
20+
21+
$locale = $notifiable->getLocale();
22+
23+
$listLines = array_filter([
24+
$locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page),
25+
$locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable),
26+
$locale->trans('notifications.detail_commenter') => $this->user->name,
27+
$locale->trans('notifications.detail_comment') => strip_tags($comment->html),
28+
]);
29+
30+
return $this->newMailMessage($locale)
31+
->subject($locale->trans('notifications.comment_mention_subject', ['pageName' => $page->getShortName()]))
32+
->line($locale->trans('notifications.comment_mention_intro', ['appName' => setting('app-name')]))
33+
->line(new ListMessageLine($listLines))
34+
->action($locale->trans('notifications.action_view_comment'), $page->getUrl('#comment' . $comment->local_id))
35+
->line($this->buildReasonFooterLine($locale));
36+
}
37+
}

app/Activity/Notifications/NotificationManager.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use BookStack\Activity\Models\Activity;
77
use BookStack\Activity\Models\Loggable;
88
use BookStack\Activity\Notifications\Handlers\CommentCreationNotificationHandler;
9+
use BookStack\Activity\Notifications\Handlers\CommentMentionNotificationHandler;
910
use BookStack\Activity\Notifications\Handlers\NotificationHandler;
1011
use BookStack\Activity\Notifications\Handlers\PageCreationNotificationHandler;
1112
use BookStack\Activity\Notifications\Handlers\PageUpdateNotificationHandler;
@@ -48,5 +49,7 @@ public function loadDefaultHandlers(): void
4849
$this->registerHandler(ActivityType::PAGE_CREATE, PageCreationNotificationHandler::class);
4950
$this->registerHandler(ActivityType::PAGE_UPDATE, PageUpdateNotificationHandler::class);
5051
$this->registerHandler(ActivityType::COMMENT_CREATE, CommentCreationNotificationHandler::class);
52+
$this->registerHandler(ActivityType::COMMENT_CREATE, CommentMentionNotificationHandler::class);
53+
$this->registerHandler(ActivityType::COMMENT_UPDATE, CommentMentionNotificationHandler::class);
5154
}
5255
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace BookStack\Activity\Tools;
4+
5+
use BookStack\Util\HtmlDocument;
6+
use DOMElement;
7+
8+
class MentionParser
9+
{
10+
public function parseUserIdsFromHtml(string $html): array
11+
{
12+
$doc = new HtmlDocument($html);
13+
14+
$ids = [];
15+
$mentionLinks = $doc->queryXPath('//a[@data-mention-user-id]');
16+
17+
foreach ($mentionLinks as $link) {
18+
if ($link instanceof DOMElement) {
19+
$id = intval($link->getAttribute('data-mention-user-id'));
20+
if ($id > 0) {
21+
$ids[] = $id;
22+
}
23+
}
24+
}
25+
26+
return array_values(array_unique($ids));
27+
}
28+
}

app/App/HomeController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public function index(
8383
if ($homepageOption === 'bookshelves') {
8484
$shelves = $this->queries->shelves->visibleForListWithCover()
8585
->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder())
86-
->paginate(18);
86+
->paginate(setting()->getInteger('lists-page-count-shelves', 18, 1, 1000));
8787
$data = array_merge($commonData, ['shelves' => $shelves]);
8888

8989
return view('home.shelves', $data);
@@ -92,7 +92,7 @@ public function index(
9292
if ($homepageOption === 'books') {
9393
$books = $this->queries->books->visibleForListWithCover()
9494
->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder())
95-
->paginate(18);
95+
->paginate(setting()->getInteger('lists-page-count-books', 18, 1, 1000));
9696
$data = array_merge($commonData, ['books' => $books]);
9797

9898
return view('home.books', $data);

app/App/Providers/AppServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace BookStack\App\Providers;
44

55
use BookStack\Access\SocialDriverManager;
6+
use BookStack\Activity\Models\Comment;
67
use BookStack\Activity\Tools\ActivityLogger;
78
use BookStack\Entities\Models\Book;
89
use BookStack\Entities\Models\Bookshelf;
@@ -73,6 +74,7 @@ public function boot(): void
7374
'book' => Book::class,
7475
'chapter' => Chapter::class,
7576
'page' => Page::class,
77+
'comment' => Comment::class,
7678
]);
7779
}
7880
}

app/App/SluggableInterface.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
/**
66
* Assigned to models that can have slugs.
77
* Must have the below properties.
8+
*
9+
* @property string $slug
810
*/
911
interface SluggableInterface
1012
{
11-
/**
12-
* Regenerate the slug for this model.
13-
*/
14-
public function refreshSlug(): string;
1513
}

0 commit comments

Comments
 (0)