summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonas Kohl2024-12-26 20:12:34 +0100
committerJonas Kohl2024-12-26 20:12:34 +0100
commit686fff945e0b4697aa74da404ce90534bb7b121d (patch)
treecb5582c8d118cb480f2978fa821950f4a2259401
parent11fed2c8ce3dd38fe686e7b27db738f64373fe3d (diff)
Add async email and topic subscribingv0.7.2
-rw-r--r--Dockerfile16
-rw-r--r--crontab1
-rw-r--r--entrypoint.sh6
-rw-r--r--src/application/actions/subscribetopic/post.php39
-rw-r--r--src/application/actions/viewtopic/get.php15
-rw-r--r--src/application/actions/viewtopic/post.php52
-rw-r--r--src/application/appdef.php2
-rw-r--r--src/application/common.php12
-rw-r--r--src/application/cron.php42
-rw-r--r--src/application/jobs/email.php33
-rw-r--r--src/application/messages/de.msg31
-rw-r--r--src/application/mystic/forum/Database.php4
-rw-r--r--src/application/mystic/forum/orm/PendingEmail.php19
-rw-r--r--src/application/mystic/forum/orm/Subscription.php14
-rw-r--r--src/application/mystic/forum/utils/StringUtils.php2
-rw-r--r--src/application/templates/bootstrap-3/view_topic.twig40
-rw-r--r--src/application/templates/modern/view_topic.twig34
-rw-r--r--src/application/templates/old/view_topic.twig29
-rw-r--r--src/index.php11
-rw-r--r--src/ui/theme-files/modern/theme.css3
-rw-r--r--src/ui/theme-files/old/subscribe.gifbin0 -> 1017 bytes
-rw-r--r--src/ui/theme-files/old/subscribe.pngbin0 -> 696 bytes
-rw-r--r--src/ui/theme-files/old/unsubscribe.gifbin0 -> 1035 bytes
-rw-r--r--src/ui/theme-files/old/unsubscribe.pngbin0 -> 778 bytes
-rw-r--r--with-env5
25 files changed, 395 insertions, 15 deletions
diff --git a/Dockerfile b/Dockerfile
index ccbf6e7..59b961a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,7 @@
FROM php:8.3-apache-bookworm AS base
RUN a2enmod rewrite
RUN apt update && apt install -y \
+ cron \
curl \
git \
libzip-dev \
@@ -21,7 +22,20 @@ RUN docker-php-ext-configure gd \
RUN docker-php-ext-install gd zip pgsql intl
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
-FROM base AS dev
+FROM base AS with-cronjob
+COPY --chmod=644 ./crontab /tmp/crontab.tmp
+RUN crontab /tmp/crontab.tmp && \
+ rm -f /tmp/crontab.tmp
+RUN touch /var/log/cron.log && \
+ touch /var/log/jobs.log && \
+ touch /var/log/job-errors.log
+
+FROM with-cronjob AS with-scripts
+COPY --chmod=700 ./entrypoint.sh /entrypoint.sh
+COPY --chmod=700 ./with-env /with-env
+
+FROM with-scripts AS dev
+CMD ["/entrypoint.sh"]
COPY ./000-default.conf /etc/apache2/sites-available/000-default.conf
WORKDIR /var/www/html
diff --git a/crontab b/crontab
new file mode 100644
index 0000000..225f276
--- /dev/null
+++ b/crontab
@@ -0,0 +1 @@
+* * * * * /with-env /usr/local/bin/php /var/www/html/application/cron.php email >/var/log/jobs.log 2>/var/log/job-errors.log
diff --git a/entrypoint.sh b/entrypoint.sh
new file mode 100644
index 0000000..f559966
--- /dev/null
+++ b/entrypoint.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+set -e
+/etc/init.d/cron start
+tail -f /var/log/jobs.log /var/log/job-errors.log &
+apache2-foreground
diff --git a/src/application/actions/subscribetopic/post.php b/src/application/actions/subscribetopic/post.php
new file mode 100644
index 0000000..78dad04
--- /dev/null
+++ b/src/application/actions/subscribetopic/post.php
@@ -0,0 +1,39 @@
+<?php
+
+/** @var ?User $currentUser */
+/** @var \mystic\forum\Database $db */
+
+use mystic\forum\orm\Subscription;
+use mystic\forum\orm\Topic;
+use mystic\forum\utils\RequestUtils;
+
+if (!$currentUser) {
+ http_response_code(403);
+ msg_error(__("You need to be logged in to delete topics!"));
+ exit;
+}
+
+$formId = "subscribetopic";
+$topicId = RequestUtils::getRequiredField("topic", $formId);
+
+$topic = new Topic();
+$topic->id = $topicId;
+
+if (!$db->fetch($topic)) {
+ http_response_code(404);
+ msg_error(__("No topic exists with this id"));
+ exit;
+}
+
+$subscription = new Subscription;
+$subscription->userId = $currentUser->id;
+$subscription->topicId = $topic->id;
+
+if ($db->fetchWhere($subscription, ["user_id", "topic_id"])) {
+ $db->delete($subscription);
+} else {
+ $subscription->id = $db->generateId();
+ $db->insert($subscription);
+}
+
+header("Location: ?_action=viewtopic&topic=" . urlencode($topic->id));
diff --git a/src/application/actions/viewtopic/get.php b/src/application/actions/viewtopic/get.php
index 636d791..56308bf 100644
--- a/src/application/actions/viewtopic/get.php
+++ b/src/application/actions/viewtopic/get.php
@@ -1,9 +1,12 @@
<?php
/** @var Post[] $posts */
+/** @var ?User $currentUser */
+/** @var \mystic\forum\Database $db */
use mystic\forum\orm\Attachment;
use mystic\forum\orm\Post;
+use mystic\forum\orm\Subscription;
use mystic\forum\orm\TopicLogMessage;
use mystic\forum\orm\User;
@@ -58,8 +61,20 @@ $allItems = array_map(function(Post|TopicLogMessage $item) use (&$db, &$topicAut
}
}, $allItems);
+$subscription = null;
+if ($currentUser !== null) {
+ $subscription = new Subscription;
+ $subscription->userId = $currentUser->id;
+ $subscription->topicId = $topic->id;
+ if (!$db->fetchWhere($subscription, ["user_id", "topic_id"])) {
+ $subscription = null;
+ }
+}
+
render("view_topic.twig", [
"topic" => $topic,
"topicAuthor" => $topicAuthor,
"allItems" => &$allItems,
+ "subscription" => $subscription,
+ "subscription_count" => count($db->fetchCustom(Subscription::class, "WHERE topic_id = $1", [ $topic->id ])),
]);
diff --git a/src/application/actions/viewtopic/post.php b/src/application/actions/viewtopic/post.php
index 1038222..b50a2e9 100644
--- a/src/application/actions/viewtopic/post.php
+++ b/src/application/actions/viewtopic/post.php
@@ -1,8 +1,18 @@
<?php
use mystic\forum\orm\Attachment;
+use mystic\forum\orm\PendingEmail;
use mystic\forum\orm\Post;
+use mystic\forum\orm\Subscription;
+use mystic\forum\orm\User;
use mystic\forum\utils\RequestUtils;
+use Symfony\Component\Mime\Address;
+
+/** @var string $formId */
+/** @var string $topicId */
+/** @var mystic\forum\orm\Topic $topic */
+/** @var \mystic\forum\Database $db */
+/** @var \mystic\forum\User $currentUser */
if (!$currentUser) {
http_response_code(403);
@@ -61,4 +71,46 @@ foreach ($attachments as $att) {
$db->insert($attachment);
}
+if (($_POST["subscribe"] ?? null) === "on") {
+ $subscription = new Subscription;
+ $subscription->userId = $currentUser->id;
+ $subscription->topicId = $topic->id;
+
+ if (!$db->fetchWhere($subscription, ["user_id", "topic_id"])) {
+ $subscription->id = $db->generateId();
+ $db->insert($subscription);
+ }
+}
+
+/** @var Subscription[] $allSubscriptions */
+$allSubscriptions = $db->fetchCustom(Subscription::class, "WHERE topic_id = $1 AND user_id <> $2", [ $topicId, $currentUser->id ]);
+
+foreach ($allSubscriptions as $subscription) {
+ $subUser = new User;
+ $subUser->id = $subscription->userId;
+ if (!$db->fetch($subUser))
+ continue;
+
+ $email = new PendingEmail;
+ $email->id = $db->generateId();
+ $email->sender = env("MAILER_FROM");
+ $email->recipient = (new Address($subUser->email, $subUser->displayName))->toString();
+ $email->subject = __("Someone made a new post in \"%topic%\"", [
+ "topic" => $topic->title,
+ ]);
+ $email->plaintextBody = __("Hello %name%,\n" .
+ "\n" .
+ "%otherName% has added the following post to \"%topic%\":\n" .
+ "%content%\n" .
+ "\n" .
+ "Click here to view: %link%\n", [
+ "name" => $subUser->displayName,
+ "otherName" => $currentUser->displayName,
+ "topic" => $topic->title,
+ "content" => quote_message(html_entity_decode(renderPostSummary($message))),
+ "link" => env("PUBLIC_URL") . "?_action=viewtopic&topic=" . urlencode($topicId) . "#post-" . urlencode($item->id),
+ ]);
+ $db->insert($email);
+}
+
header("Location: ?_action=viewtopic&topic=" . urlencode($topicId) . "#form");
diff --git a/src/application/appdef.php b/src/application/appdef.php
index 1f51d97..d623cd1 100644
--- a/src/application/appdef.php
+++ b/src/application/appdef.php
@@ -1,3 +1,3 @@
<?php
-const MYSTICBB_VERSION = "0.7.1";
+const MYSTICBB_VERSION = "0.7.2";
diff --git a/src/application/common.php b/src/application/common.php
new file mode 100644
index 0000000..113d598
--- /dev/null
+++ b/src/application/common.php
@@ -0,0 +1,12 @@
+<?php
+
+use mystic\forum\Database;
+use mystic\forum\exceptions\DatabaseConnectionException;
+
+function get_db(): Database {
+ return new Database(Database::getConnectionString("db", getenv("POSTGRES_USER"), getenv("POSTGRES_PASSWORD"), getenv("POSTGRES_DBNAME")));
+}
+
+function quote_message(string $message): string {
+ return implode("\n", array_map(fn($ln) => "> $ln", preg_split('~(*BSR_ANYCRLF)\R~', trim($message))));
+}
diff --git a/src/application/cron.php b/src/application/cron.php
new file mode 100644
index 0000000..094fe50
--- /dev/null
+++ b/src/application/cron.php
@@ -0,0 +1,42 @@
+<?php
+
+const __ROOT__ = __DIR__ . "/..";
+
+require_once __ROOT__ . "/vendor/autoload.php";
+require_once __ROOT__ . "/application/i18n.php";
+require_once __ROOT__ . "/application/common.php";
+
+function print_error(string $msg): void {
+ file_put_contents("php://stderr", $msg);
+}
+
+function check_sapi(): void {
+ if (PHP_SAPI !== "cli") {
+ http_response_code(500);
+ echo "Error: Can only be called via command line\n";
+ exit(1);
+ }
+}
+
+check_sapi();
+
+$job = $argv[1] ?? null;
+
+if ($job === null) {
+ print_error("Error: No job specified\nUsage: php cron.php <job>\n");
+ exit(1);
+}
+
+$jobsDir = __DIR__ . "/jobs";
+
+$availableJobs = array_map(fn($i) => pathinfo($i, PATHINFO_FILENAME), array_values(array_filter(scandir($jobsDir),
+ fn($i) => is_file($jobsDir . "/" . $i) && $i[0] !== "." && pathinfo($i, PATHINFO_EXTENSION) === "php")));
+
+if (!in_array($job, $availableJobs)) {
+ print_error("Error: Invalid job specified\nAvailable jobs are:\n" . implode("", array_map(fn($i) => " $i\n", $availableJobs)));
+ exit(1);
+}
+
+$jobFile = $jobsDir . "/" . $job . ".php";
+
+include $jobFile;
diff --git a/src/application/jobs/email.php b/src/application/jobs/email.php
new file mode 100644
index 0000000..8bb97dc
--- /dev/null
+++ b/src/application/jobs/email.php
@@ -0,0 +1,33 @@
+<?php
+
+use mystic\forum\orm\PendingEmail;
+use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
+use Symfony\Component\Mailer\Transport;
+use Symfony\Component\Mime\Email;
+
+check_sapi();
+
+$db = get_db();
+
+/** @var PendingEmail[] $pendingEmails */
+$pendingEmails = $db->fetchAll(PendingEmail::class);
+
+$transport = Transport::fromDsn(getenv("MAILER_DSN"));
+
+foreach ($pendingEmails as $pendingEmail) {
+ try {
+ $mail = (new Email)
+ ->from($pendingEmail->sender)
+ ->to($pendingEmail->recipient)
+ ->subject($pendingEmail->subject)
+ ;
+ if (!empty($pendingEmail->htmlBody))
+ $mail->html($pendingEmail->htmlBody);
+ if (!empty($pendingEmail->plaintextBody))
+ $mail->text($pendingEmail->plaintextBody);
+ $transport->send($mail);
+ } catch (TransportExceptionInterface $ex) {
+ print_error("Failed to send mail: " . $ex->getMessage() . "\n");
+ }
+ $db->delete($pendingEmail);
+}
diff --git a/src/application/messages/de.msg b/src/application/messages/de.msg
index 3ce96d4..c16d1d1 100644
--- a/src/application/messages/de.msg
+++ b/src/application/messages/de.msg
@@ -540,3 +540,34 @@ metadata({
: "Page generation took %dur% second(s)"
= "Seitenaufbau dauerte %dur% Sekunde(n)"
+
+: "Unsubscribe from topic"
+= "Thema deabonnieren"
+
+: "Subscribe to topic"
+= "Thema abonnieren"
+
+: "Someone made a new post in \"%topic%\""
+= "Jemand hat einen neuen Beitrag zu \"%topic%\" hinzugefügt"
+
+: "Hello %name%,\n"
+ "\n"
+ "%otherName% has added the following post to \"%topic%\":\n"
+ "%content%\n"
+ "\n"
+ "Click here to view: %link%\n"
+= "Hallo %name%,\n"
+ "\n"
+ "%otherName% hat den folgenden Beitrag zu \"%topic%\" hinzugefügt:\n"
+ "%content%\n"
+ "\n"
+ "Den Beitrag ansehen: %link%\n"
+
+:...
+- "%n% person is watching this topic"
+- "%n% people are watching this topic"
+- "Nobody is watching this topic"
+=...
+- "%n% Person beobachtet dieses Thema"
+- "%n% Personen beobachten dieses Thema"
+- "Niemand beobachtet dieses Thema"
diff --git a/src/application/mystic/forum/Database.php b/src/application/mystic/forum/Database.php
index 1c2d710..bca4ac9 100644
--- a/src/application/mystic/forum/Database.php
+++ b/src/application/mystic/forum/Database.php
@@ -46,12 +46,12 @@ class Database {
protected function queryParams(string $query, array $params): Result|false {
++$this->queryCount;
- return \pg_query_params ($this->connection, $query, $params);
+ return \pg_query_params($this->connection, $query, $params);
}
protected function query(string $query): Result|false {
++$this->queryCount;
- return \pg_query ($this->connection, $query);
+ return \pg_query($this->connection, $query);
}
public static function generateId(int $length = 64): string {
diff --git a/src/application/mystic/forum/orm/PendingEmail.php b/src/application/mystic/forum/orm/PendingEmail.php
new file mode 100644
index 0000000..70622b3
--- /dev/null
+++ b/src/application/mystic/forum/orm/PendingEmail.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace mystic\forum\orm;
+
+use mystic\forum\attributes\PrimaryKey;
+use mystic\forum\attributes\Table;
+use mystic\forum\orm\Entity;
+
+#[Table("public.emails")]
+class PendingEmail extends Entity {
+ #[PrimaryKey] public string $id;
+ public string $sender;
+ public string $recipient;
+ public string $subject;
+ public ?string $htmlBody;
+ public ?string $plaintextBody;
+
+ // TODO Attachments
+}
diff --git a/src/application/mystic/forum/orm/Subscription.php b/src/application/mystic/forum/orm/Subscription.php
new file mode 100644
index 0000000..6934685
--- /dev/null
+++ b/src/application/mystic/forum/orm/Subscription.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace mystic\forum\orm;
+
+use mystic\forum\attributes\PrimaryKey;
+use mystic\forum\attributes\References;
+use mystic\forum\attributes\Table;
+
+#[Table("public.subscriptions")]
+class Subscription extends Entity {
+ #[PrimaryKey] public string $id;
+ #[References("public.topics", onDelete: References::CASCADE)] public string $topicId;
+ #[References("public.users", onDelete: References::CASCADE)] public string $userId;
+}
diff --git a/src/application/mystic/forum/utils/StringUtils.php b/src/application/mystic/forum/utils/StringUtils.php
index fd38915..94221fb 100644
--- a/src/application/mystic/forum/utils/StringUtils.php
+++ b/src/application/mystic/forum/utils/StringUtils.php
@@ -23,6 +23,8 @@ final class StringUtils {
}
public static function truncate(string $str, int $maxLength, string $ellipsis = "…"): string {
+ if ($maxLength < 0)
+ return $str;
return mb_strimwidth($str, 0, $maxLength, $ellipsis);
}
}
diff --git a/src/application/templates/bootstrap-3/view_topic.twig b/src/application/templates/bootstrap-3/view_topic.twig
index 383dd41..7009afc 100644
--- a/src/application/templates/bootstrap-3/view_topic.twig
+++ b/src/application/templates/bootstrap-3/view_topic.twig
@@ -30,6 +30,12 @@
or currentUser.hasPermission(permission("DELETE_OTHER_TOPIC"))
) %}
+{% set canSubscribe = currentUser is not null %}
+
+{% set isSubscribed =
+ currentUser is not null
+ and ctx.subscription is not null %}
+
{% set title = ctx.topic.title %}
{% extends "base.twig" %}
@@ -178,30 +184,40 @@
{{ ctx.topic.title }}
<div class="pull-right text-normal">
{% if canEdit and not ctx.topic.isLocked %}
- <button id="btn-edit-title" class="btn btn-default js-only"><span class="fa fa-pencil" aria-hidden="true"></span> {{ __("Edit title") }}</button>
+ <button id="btn-edit-title" class="btn btn-default btn-xs js-only"><span class="fa fa-pencil" aria-hidden="true"></span> {{ __("Edit title") }}</button>
{% endif %}
{% if canReply %}
- <button id="btn-reply" class="btn btn-default js-only"><span class="fa fa-comment" aria-hidden="true"></span> {{ __("Reply") }}</button>
+ <button id="btn-reply" class="btn btn-default btn-xs js-only"><span class="fa fa-comment" aria-hidden="true"></span> {{ __("Reply") }}</button>
+ {% endif %}
+ {% if canSubscribe %}
+ <form action="?_action=subscribetopic" method="post" class="seamless-inline">
+ <input type="hidden" name="topic" value="{{ ctx.topic.id }}">
+ {% if isSubscribed %}
+ <button type="submit" class="btn btn-xs btn-default"><span class="fa fa-bell-slash" aria-hidden="true"></span> {{ __("Unsubscribe from topic") }}</button>
+ {% else %}
+ <button type="submit" class="btn btn-xs btn-default"><span class="fa fa-bell" aria-hidden="true"></span> {{ __("Subscribe to topic") }}</button>
+ {% endif %}
+ </form>
{% endif %}
{% if canEdit %}
{% if ctx.topic.isLocked %}
<form action="?_action=locktopic" method="post" class="seamless-inline">
<input type="hidden" name="topic" value="{{ ctx.topic.id }}">
<input type="hidden" name="locked" value="false">
- <button type="submit" class="btn btn-success"><span class="fa fa-unlock" aria-hidden="true"></span> {{ __("Unlock topic") }}</button>
+ <button type="submit" class="btn btn-success btn-xs"><span class="fa fa-unlock" aria-hidden="true"></span> {{ __("Unlock topic") }}</button>
</form>
{% else %}
<form action="?_action=locktopic" method="post" class="seamless-inline">
<input type="hidden" name="topic" value="{{ ctx.topic.id }}">
<input type="hidden" name="locked" value="true">
- <button type="submit" class="btn btn-warning"><span class="fa fa-lock" aria-hidden="true"></span> {{ __("Lock topic") }}</button>
+ <button type="submit" class="btn btn-warning btn-xs"><span class="fa fa-lock" aria-hidden="true"></span> {{ __("Lock topic") }}</button>
</form>
{% endif %}
{% endif %}
{% if canDelete %}
<form action="?_action=deletetopic" method="post" class="seamless-inline">
<input type="hidden" name="topic" value="{{ ctx.topic.id }}">
- <button type="submit" class="btn btn-danger"><span class="fa fa-trash" aria-hidden="true"></span> {{ __("Delete topic") }}</button>
+ <button type="submit" class="btn btn-danger btn-xs"><span class="fa fa-trash" aria-hidden="true"></span> {{ __("Delete topic") }}</button>
</form>
{% endif %}
</div>
@@ -209,7 +225,14 @@
{{ __("Started by %user% on %date%", {
"user": (ctx.topicAuthor is not null) ? ('<a href="?_action=viewuser&amp;user=' ~ ctx.topicAuthor.id|url_encode|e("html") ~ '">' ~ ctx.topicAuthor.displayName|e("html") ~ '</a>') : __("(deleted)"),
"date": '<span class="_time">' ~ ctx.topic.creationDate.format("c")|e("html") ~ '</span>',
- }) }}
+ }) }} &bull; {{ ___(
+ "%n% person is watching this topic",
+ "%n% people are watching this topic",
+ ctx.subscription_count,
+ {
+ n: ctx.subscription_count,
+ },
+ ) }}
</div>
{% if canEdit %}
<form action="?_action=updatetopic" method="post" id="editHeading" style="display: none;" class="form-inline seamless-inline">
@@ -351,6 +374,11 @@ $(function() {
}) }}</label>
<input type="file" name="files[]" id="i_files" multiple accept="*/*">
</div>
+ {% if not isSubscribed %}
+ <div class="checkbox">
+ <label><input type="checkbox" name="subscribe" value="on" checked> {{ __("Subscribe to topic") }}</label>
+ </div>
+ {% endif %}
<button type="submit" class="btn btn-success">{{ __("Post reply") }} <span class="fa fa-send" aria-hidden="true"></span></button>
</form>
{% else %}
diff --git a/src/application/templates/modern/view_topic.twig b/src/application/templates/modern/view_topic.twig
index 733ce1b..c064b1c 100644
--- a/src/application/templates/modern/view_topic.twig
+++ b/src/application/templates/modern/view_topic.twig
@@ -30,6 +30,12 @@
or currentUser.hasPermission(permission("DELETE_OTHER_TOPIC"))
) %}
+{% set canSubscribe = currentUser is not null %}
+
+{% set isSubscribed =
+ currentUser is not null
+ and ctx.subscription is not null %}
+
{% set title = ctx.topic.title %}
{% extends "base.twig" %}
@@ -189,6 +195,20 @@
<svg viewBox="0 0 24 24" class="icon"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg>
</button>
{% endif %}
+ {% if canSubscribe %}
+ <form action="?_action=subscribetopic" method="post" class="seamless-inline">
+ <input type="hidden" name="topic" value="{{ ctx.topic.id }}">
+ {% if isSubscribed %}
+ <button type="submit" class="btn btn-iconic" title="{{ __("Unsubscribe from topic") }}">
+ <svg viewBox="0 0 24 24" class="icon"><path d="M10.268 21a2 2 0 0 0 3.464 0" /><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></svg>
+ </button>
+ {% else %}
+ <button type="submit" class="btn btn-iconic" title="{{ __("Subscribe to topic") }}">
+ <svg viewBox="0 0 24 24" class="icon"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M17 17H4a1 1 0 0 1-.74-1.673C4.59 13.956 6 12.499 6 8a6 6 0 0 1 .258-1.742"/><path d="m2 2 20 20"/><path d="M8.668 3.01A6 6 0 0 1 18 8c0 2.687.77 4.653 1.707 6.05"/></svg>
+ </button>
+ {% endif %}
+ </form>
+ {% endif %}
{% if canEdit %}
{% if ctx.topic.isLocked %}
<form action="?_action=locktopic" method="post">
@@ -221,7 +241,14 @@
{{ __("Started by %user% on %date%", {
"user": (ctx.topicAuthor is not null) ? ('<a href="?_action=viewuser&amp;user=' ~ ctx.topicAuthor.id|url_encode|e("html") ~ '">' ~ ctx.topicAuthor.displayName|e("html") ~ '</a>') : __("(deleted)"),
"date": '<span class="_time">' ~ ctx.topic.creationDate.format("c")|e("html") ~ '</span>',
- }) }}
+ }) }} &bull; {{ ___(
+ "%n% person is watching this topic",
+ "%n% people are watching this topic",
+ ctx.subscription_count,
+ {
+ n: ctx.subscription_count,
+ },
+ ) }}
</div>
{% if canEdit %}
<form action="?_action=updatetopic" method="post" id="editHeading" hidden>
@@ -348,6 +375,11 @@ document.addEventListener("DOMContentLoaded", function() {
}) }}</label>
<input type="file" name="files[]" id="i_files" multiple accept="*/*">
</div>
+ {% if not isSubscribed %}
+ <div class="checkbox">
+ <label><input type="checkbox" name="subscribe" value="on" checked> {{ __("Subscribe to topic") }}</label>
+ </div>
+ {% endif %}
<button type="submit" class="btn btn-success">
<span>{{ __("Post reply") }}</span>
<svg viewBox="0 0 24 24" class="icon"><path d="M3.714 3.048a.498.498 0 0 0-.683.627l2.843 7.627a2 2 0 0 1 0 1.396l-2.842 7.627a.498.498 0 0 0 .682.627l18-8.5a.5.5 0 0 0 0-.904z"/><path d="M6 12h16"/></svg>
diff --git a/src/application/templates/old/view_topic.twig b/src/application/templates/old/view_topic.twig
index 613de49..70d0d67 100644
--- a/src/application/templates/old/view_topic.twig
+++ b/src/application/templates/old/view_topic.twig
@@ -30,6 +30,12 @@
or currentUser.hasPermission(permission("DELETE_OTHER_TOPIC"))
) %}
+{% set canSubscribe = currentUser is not null %}
+
+{% set isSubscribed =
+ currentUser is not null
+ and ctx.subscription is not null %}
+
{% set title = ctx.topic.title %}
{% extends "base.twig" %}
@@ -76,6 +82,16 @@
#}</form>
{%- endif -%}
{%- endif -%}
+ {%- if canSubscribe -%}
+ <form action="?_action=subscribetopic" method="post" class="inline">{#
+ #}<input type="hidden" name="topic" value="{{ ctx.topic.id }}">
+ {%- if isSubscribed -%}
+ <button type="submit" class="seamless m-r"><img src="/ui/theme-files/old/unsubscribe.gif" width="16" height="16" draggable="false" alt="{{ __("Unsubscribe from topic") }}"></button>
+ {%- else -%}
+ <button type="submit" class="seamless m-r"><img src="/ui/theme-files/old/subscribe.gif" width="16" height="16" draggable="false" alt="{{ __("Subscribe to topic") }}"></button>
+ {%- endif -%}
+ </form>
+ {%- endif -%}
{%- if canDelete -%}
<form action="?_action=deletetopic" method="post" class="inline">{#
#}<input type="hidden" name="topic" value="{{ ctx.topic.id }}">{#
@@ -87,7 +103,14 @@
{{ __("Started by %user% on %date%", {
"user": (ctx.topicAuthor is not null) ? ('<a href="?_action=viewuser&amp;user=' ~ ctx.topicAuthor.id|url_encode|e("html") ~ '">' ~ ctx.topicAuthor.displayName|e("html") ~ '</a>') : __("(deleted)"),
"date": '<span class="_time">' ~ ctx.topic.creationDate.format("c")|e("html") ~ '</span>',
- }) }}
+ }) }} &bull; {{ ___(
+ "%n% person is watching this topic",
+ "%n% people are watching this topic",
+ ctx.subscription_count,
+ {
+ n: ctx.subscription_count,
+ },
+ ) }}
</div>
{% if canEdit %}
<form action="?_action=updatetopic" method="post" id="editHeading" style="display: none;" class="inline">
@@ -216,6 +239,10 @@ $(function() {
}) }}</label><br>
<input type="file" name="files[]" id="i_files" multiple accept="*/*"><br>
<br>
+ {% if not isSubscribed %}
+ <label><input type="checkbox" name="subscribe" value="on" checked> {{ __("Subscribe to topic") }}</label><br>
+ <br>
+ {% endif %}
<button type="submit">{{ __("Post reply") }}</button>
</form>
{% else %}
diff --git a/src/index.php b/src/index.php
index 8036a19..0580523 100644
--- a/src/index.php
+++ b/src/index.php
@@ -9,6 +9,8 @@ use mystic\forum\orm\Topic;
use mystic\forum\orm\TopicLogMessage;
use mystic\forum\orm\User;
use mystic\forum\orm\UserPermissions;
+use mystic\forum\orm\PendingEmail;
+use mystic\forum\orm\Subscription;
use mystic\forum\utils\RequestUtils;
use mystic\forum\utils\StringUtils;
use Twig\Environment;
@@ -244,11 +246,11 @@ function expandTags(string $contents): string {
}, $contents);
}
-function renderPostSummary(string $contents): string {
+function renderPostSummary(string $contents, int $maxLength = 100): string {
$contents = renderPost($contents);
// remove spoiler contents so they don't appear in summaries
$contents = preg_replace('/<!--##SPOILER_START##-->(.*?)<!--##SPOILER_END##-->/s', '[ ' . __("Spoiler") . ' ]', $contents);
- return htmlentities(html_entity_decode(StringUtils::truncate(strip_tags($contents), 100)));
+ return htmlentities(html_entity_decode(StringUtils::truncate(strip_tags($contents), $maxLength)));
}
function renderPost(string $contents): string {
@@ -299,6 +301,7 @@ function env(string $key): ?string {
require_once __DIR__ . "/vendor/autoload.php";
require_once __DIR__ . "/application/i18n.php";
+require_once __DIR__ . "/application/common.php";
if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["lang"])) {
parse_str($_SERVER["QUERY_STRING"], $query);
@@ -319,7 +322,7 @@ i18n_locale($user_locale);
$db = null;
try {
- $db = new Database(Database::getConnectionString("db", getenv("POSTGRES_USER"), getenv("POSTGRES_PASSWORD"), getenv("POSTGRES_DBNAME")));
+ $db = get_db();
} catch (DatabaseConnectionException $ex) {
msg_error(
__("Failed to connect to database:\n%details%", [
@@ -337,6 +340,8 @@ $db->ensureTable(Topic::class);
$db->ensureTable(Post::class);
$db->ensureTable(Attachment::class);
$db->ensureTable(TopicLogMessage::class);
+$db->ensureTable(PendingEmail::class);
+$db->ensureTable(Subscription::class);
$superuser = new User();
$superuser->id = "SUPERUSER";
diff --git a/src/ui/theme-files/modern/theme.css b/src/ui/theme-files/modern/theme.css
index 1f1cc3a..3a5f4af 100644
--- a/src/ui/theme-files/modern/theme.css
+++ b/src/ui/theme-files/modern/theme.css
@@ -224,6 +224,9 @@ footer {
display: block;
}
}
+.checkbox {
+ margin-block: 8px;
+}
.form-inline {
display: inline-flex;
margin: 0;
diff --git a/src/ui/theme-files/old/subscribe.gif b/src/ui/theme-files/old/subscribe.gif
new file mode 100644
index 0000000..195067c
--- /dev/null
+++ b/src/ui/theme-files/old/subscribe.gif
Binary files differ
diff --git a/src/ui/theme-files/old/subscribe.png b/src/ui/theme-files/old/subscribe.png
new file mode 100644
index 0000000..2d3049d
--- /dev/null
+++ b/src/ui/theme-files/old/subscribe.png
Binary files differ
diff --git a/src/ui/theme-files/old/unsubscribe.gif b/src/ui/theme-files/old/unsubscribe.gif
new file mode 100644
index 0000000..27f230a
--- /dev/null
+++ b/src/ui/theme-files/old/unsubscribe.gif
Binary files differ
diff --git a/src/ui/theme-files/old/unsubscribe.png b/src/ui/theme-files/old/unsubscribe.png
new file mode 100644
index 0000000..396f009
--- /dev/null
+++ b/src/ui/theme-files/old/unsubscribe.png
Binary files differ
diff --git a/with-env b/with-env
new file mode 100644
index 0000000..aabba50
--- /dev/null
+++ b/with-env
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+set -e
+. <(xargs -0 bash -c 'printf "export %q\n" "$@"' -- < /proc/1/environ)
+$@