diff options
Diffstat (limited to 'src')
21 files changed, 368 insertions, 14 deletions
| 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&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>', -        }) }} +        }) }} • {{ ___( +            "%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&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>', -        }) }} +        }) }} • {{ ___( +            "%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&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>', -        }) }} +        }) }} • {{ ___( +            "%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.gifBinary files differ new file mode 100644 index 0000000..195067c --- /dev/null +++ b/src/ui/theme-files/old/subscribe.gif diff --git a/src/ui/theme-files/old/subscribe.png b/src/ui/theme-files/old/subscribe.pngBinary files differ new file mode 100644 index 0000000..2d3049d --- /dev/null +++ b/src/ui/theme-files/old/subscribe.png diff --git a/src/ui/theme-files/old/unsubscribe.gif b/src/ui/theme-files/old/unsubscribe.gifBinary files differ new file mode 100644 index 0000000..27f230a --- /dev/null +++ b/src/ui/theme-files/old/unsubscribe.gif diff --git a/src/ui/theme-files/old/unsubscribe.png b/src/ui/theme-files/old/unsubscribe.pngBinary files differ new file mode 100644 index 0000000..396f009 --- /dev/null +++ b/src/ui/theme-files/old/unsubscribe.png |