diff --git a/src/main/java/smithereen/SmithereenApplication.java b/src/main/java/smithereen/SmithereenApplication.java index 2163b931..0301c751 100644 --- a/src/main/java/smithereen/SmithereenApplication.java +++ b/src/main/java/smithereen/SmithereenApplication.java @@ -478,6 +478,7 @@ public static void main(String[] args){ getLoggedIn("/outbox", MailRoutes::outbox); getLoggedIn("/compose", MailRoutes::compose); postWithCSRF("/send", MailRoutes::sendMessage); + getLoggedIn("/history", MailRoutes::history); path("/messages/:id", ()->{ Filter idParserFilter=(req, resp)->{ long id=Utils.decodeLong(req.params(":id")); diff --git a/src/main/java/smithereen/controllers/MailController.java b/src/main/java/smithereen/controllers/MailController.java index 177bec19..e755141e 100644 --- a/src/main/java/smithereen/controllers/MailController.java +++ b/src/main/java/smithereen/controllers/MailController.java @@ -344,4 +344,12 @@ public void putForeignMessage(MailMessage msg){ throw new InternalServerErrorException(x); } } + + public PaginatedList getHistory(User self, User peer, int offset, int count){ + try{ + return MailStorage.getHistory(self.id, peer.id, offset, count); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } } diff --git a/src/main/java/smithereen/lang/Lang.java b/src/main/java/smithereen/lang/Lang.java index 14822510..709f0bad 100644 --- a/src/main/java/smithereen/lang/Lang.java +++ b/src/main/java/smithereen/lang/Lang.java @@ -24,10 +24,10 @@ import java.util.Map; import smithereen.Utils; -import smithereen.model.User; import smithereen.lang.formatting.ICUMessageParser; import smithereen.lang.formatting.ICUMessageSyntaxException; import smithereen.lang.formatting.StringTemplate; +import smithereen.model.User; import spark.utils.StringUtils; public class Lang{ @@ -242,6 +242,15 @@ public String formatTime(Instant time, ZoneId timeZone){ return String.format(locale, "%d:%02d", dt.getHour(), dt.getMinute()); } + public String formatTimeOrDay(Instant time, ZoneId timeZone){ + ZonedDateTime dt=time.atZone(timeZone); + if(dt.toLocalDate().equals(LocalDate.now(timeZone))){ + return String.format(locale, "%d:%02d:%02d", dt.getHour(), dt.getMinute(), dt.getSecond()); + }else{ + return String.format(locale, "%02d.%02d.%02d", dt.getDayOfMonth(), dt.getMonthValue(), dt.getYear()%100); + } + } + public String getAsJS(String key){ if(!data.containsKey(key)){ if(fallback!=null) diff --git a/src/main/java/smithereen/routes/MailRoutes.java b/src/main/java/smithereen/routes/MailRoutes.java index 7625f97b..f4be985c 100644 --- a/src/main/java/smithereen/routes/MailRoutes.java +++ b/src/main/java/smithereen/routes/MailRoutes.java @@ -69,6 +69,7 @@ public static Object viewMessage(Request req, Response resp, Account self, Appli model.with("tab", "view").with("message", msg).with("users", users); boolean isOutgoing=msg.senderID==self.user.id; User peer=users.get(isOutgoing ? msg.to.iterator().next() : msg.senderID); + model.with("peer", peer); model.pageTitle(lang(req).get(isOutgoing ? "mail_message_title_outgoing" : "mail_message_title_incoming", Map.of("name", peer.getFirstLastAndGender()))); if(StringUtils.isNotEmpty(msg.subject)){ String subject=msg.subject; @@ -246,4 +247,18 @@ public static Object restore(Request req, Response resp, Account self, Applicati .remove("msgDeletedRow"+msg.encodedID) .show(origElementID); } + + public static Object history(Request req, Response resp, Account self, ApplicationContext ctx){ + if(!isAjax(req)){ + resp.redirect("/my/mail"); + return ""; + } + requireQueryParams(req, "peer"); + int peerID=safeParseInt(req.queryParams("peer")); + User peer=ctx.getUsersController().getUserOrThrow(peerID); + RenderedTemplateResponse model=new RenderedTemplateResponse("mail_history", req); + model.paginate(ctx.getMailController().getHistory(self.user, peer, offset(req), 50)); + model.with("users", Map.of(self.user.id, self.user, peerID, peer)); + return new WebDeltaResponse(resp).setContent("mailHistoryWrap", model.renderToString()); + } } diff --git a/src/main/java/smithereen/storage/MailStorage.java b/src/main/java/smithereen/storage/MailStorage.java index 315b2866..189f2c91 100644 --- a/src/main/java/smithereen/storage/MailStorage.java +++ b/src/main/java/smithereen/storage/MailStorage.java @@ -1,6 +1,7 @@ package smithereen.storage; import java.net.URI; +import java.sql.PreparedStatement; import java.sql.SQLException; import java.time.Instant; import java.util.Collection; @@ -241,4 +242,18 @@ public static void consumePrivacyGrant(int ownerID, int userID) throws SQLExcept .where("owner_id=? AND user_id=?", ownerID, userID) .executeNoResult(); } + + public static PaginatedList getHistory(int ownerID, int peerID, int offset, int count) throws SQLException{ + try(DatabaseConnection conn=DatabaseConnectionManager.getConnection()){ + PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT COUNT(*) FROM mail_messages_peers JOIN mail_messages ON message_id=mail_messages.id" + + " WHERE mail_messages_peers.owner_id=? AND mail_messages_peers.peer_id=? AND mail_messages.deleted_at IS NULL", ownerID, peerID); + int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery()); + if(total==0) + return PaginatedList.emptyList(count); + stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT mail_messages.* FROM mail_messages_peers JOIN mail_messages ON message_id=mail_messages.id" + + " WHERE mail_messages_peers.owner_id=? AND mail_messages_peers.peer_id=? AND mail_messages.deleted_at IS NULL ORDER BY message_id DESC LIMIT ? OFFSET ?", ownerID, peerID, count, offset); + List msgs=DatabaseUtils.resultSetToObjectStream(stmt.executeQuery(), MailMessage::fromResultSet, null).toList(); + return new PaginatedList<>(msgs, total, offset, count); + } + } } diff --git a/src/main/java/smithereen/templates/LangDateFunction.java b/src/main/java/smithereen/templates/LangDateFunction.java index 9af35b3a..3503ae60 100644 --- a/src/main/java/smithereen/templates/LangDateFunction.java +++ b/src/main/java/smithereen/templates/LangDateFunction.java @@ -20,17 +20,28 @@ public class LangDateFunction implements Function{ public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber){ Object arg=args.get("date"); boolean forceAbsolute=(Boolean) args.getOrDefault("forceAbsolute", Boolean.FALSE); + Lang lang=Lang.get(context.getLocale()); + ZoneId timeZone=(ZoneId) context.getVariable("timeZone"); if(arg instanceof java.sql.Date sd) - return Lang.get(context.getLocale()).formatDay(sd.toLocalDate()); + return lang.formatDay(sd.toLocalDate()); if(arg instanceof LocalDate ld) - return forceAbsolute ? Lang.get(context.getLocale()).formatDay(ld) : Lang.get(context.getLocale()).formatDayRelative(ld, (ZoneId) context.getVariable("timeZone")); - if(arg instanceof Instant instant) - return Lang.get(context.getLocale()).formatDate(instant, (ZoneId) context.getVariable("timeZone"), forceAbsolute); + return forceAbsolute ? lang.formatDay(ld) : lang.formatDayRelative(ld, timeZone); + if(arg instanceof Instant instant){ + String format=(String) args.get("format"); + if(format!=null){ + switch(format){ + case "timeOrDay" -> { + return lang.formatTimeOrDay(instant, timeZone); + } + } + } + return lang.formatDate(instant, timeZone, forceAbsolute); + } return "????"; } @Override public List getArgumentNames(){ - return List.of("date", "forceAbsolute"); + return List.of("date", "forceAbsolute", "format"); } } diff --git a/src/main/java/smithereen/templates/RandomStringFunction.java b/src/main/java/smithereen/templates/RandomStringFunction.java new file mode 100644 index 00000000..0a9eeaff --- /dev/null +++ b/src/main/java/smithereen/templates/RandomStringFunction.java @@ -0,0 +1,22 @@ +package smithereen.templates; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import io.pebbletemplates.pebble.extension.Function; +import io.pebbletemplates.pebble.template.EvaluationContext; +import io.pebbletemplates.pebble.template.PebbleTemplate; +import smithereen.Utils; + +public class RandomStringFunction implements Function{ + @Override + public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber){ + return Utils.randomAlphanumericString((Integer)args.getOrDefault("length", 10)); + } + + @Override + public List getArgumentNames(){ + return List.of("length"); + } +} diff --git a/src/main/java/smithereen/templates/SmithereenExtension.java b/src/main/java/smithereen/templates/SmithereenExtension.java index 392114cb..ae6f163d 100644 --- a/src/main/java/smithereen/templates/SmithereenExtension.java +++ b/src/main/java/smithereen/templates/SmithereenExtension.java @@ -22,7 +22,8 @@ public Map getFunctions(){ "getTime", new InstantToTimeFunction(), "getDate", new InstantToDateFunction(), "describeAttachments", new DescribeAttachmentsFunction(), - "addQueryParams", new AddQueryParamsFunction() + "addQueryParams", new AddQueryParamsFunction(), + "randomString", new RandomStringFunction() ); } diff --git a/src/main/resources/langs/en/mail.json b/src/main/resources/langs/en/mail.json index 8c5a3f4f..575b4b97 100644 --- a/src/main/resources/langs/en/mail.json +++ b/src/main/resources/langs/en/mail.json @@ -33,5 +33,6 @@ "mail_in_reply_to_own_post": "your post", "mail_in_reply_to_own_comment": "your comment", "profile_write_message": "Send a message", - "messages_title": "Messages" + "messages_title": "Messages", + "mail_conversation_history": "Conversation history" } \ No newline at end of file diff --git a/src/main/resources/langs/ru/mail.json b/src/main/resources/langs/ru/mail.json index b5f60d89..7d5bdaf3 100644 --- a/src/main/resources/langs/ru/mail.json +++ b/src/main/resources/langs/ru/mail.json @@ -33,5 +33,6 @@ "mail_in_reply_to_own_post": "вашу запись", "mail_in_reply_to_own_comment": "ваш комментарий", "profile_write_message": "Написать сообщение", - "messages_title": "Сообщения" + "messages_title": "Сообщения", + "mail_conversation_history": "История сообщений" } \ No newline at end of file diff --git a/src/main/resources/templates/common/pagination.twig b/src/main/resources/templates/common/pagination.twig index b751442e..38deae55 100644 --- a/src/main/resources/templates/common/pagination.twig +++ b/src/main/resources/templates/common/pagination.twig @@ -10,12 +10,17 @@ {#- NB: curPage starts at 0, users expect pages to start at 1 -#} {% if totalPages>1 %} {% endif %} \ No newline at end of file diff --git a/src/main/resources/templates/desktop/mail_history.twig b/src/main/resources/templates/desktop/mail_history.twig new file mode 100644 index 00000000..63ae224d --- /dev/null +++ b/src/main/resources/templates/desktop/mail_history.twig @@ -0,0 +1,14 @@ +
+
+
{{ L('mail_conversation_history') }}
+ {% include "pagination" with {'paginationAjax': true} %} +
+ {% for msg in items %} +
+ +
{{ msg.text | postprocessHTML }}{% if msg.attachments is not empty %}{{ renderAttachments(msg.processedAttachments, null) }}{% endif %}
+ +
+ {% endfor %} +
{% include "pagination" with {'paginationAjax': true} %}
+
\ No newline at end of file diff --git a/src/main/resources/templates/desktop/mail_message.twig b/src/main/resources/templates/desktop/mail_message.twig index ed0ac1d2..2c98e393 100644 --- a/src/main/resources/templates/desktop/mail_message.twig +++ b/src/main/resources/templates/desktop/mail_message.twig @@ -12,7 +12,7 @@ -
{{ LD(message.createdAt) }}{% if message.updatedAt is not null %} | {{ L('mail_edited_at', {'time': LD(message.updatedAt)}) }}{% endif %}
+
{{ LD(message.createdAt, true) }}{% if message.updatedAt is not null %} | {{ L('mail_edited_at', {'time': LD(message.updatedAt)}) }}{% endif %}
{{ users[message.senderID] | pictureForAvatar('s') }}
{{ L('mail_from') }}:
@@ -55,5 +55,11 @@
+{% if peer is not null %} + +{% endif %} {% endblock %} diff --git a/src/main/resources/templates/mobile/mail_message.twig b/src/main/resources/templates/mobile/mail_message.twig index 03dbc31c..935c7e1b 100644 --- a/src/main/resources/templates/mobile/mail_message.twig +++ b/src/main/resources/templates/mobile/mail_message.twig @@ -11,7 +11,7 @@ {{ users[message.senderID] | pictureForAvatar('s') }}
-
{{ LD(message.createdAt) }}{% if message.updatedAt is not null %} | {{ L('mail_edited_at', {'time': LD(message.updatedAt)}) }}{% endif %}
+
{{ LD(message.createdAt, true) }}{% if message.updatedAt is not null %} | {{ L('mail_edited_at', {'time': LD(message.updatedAt)}) }}{% endif %}
diff --git a/src/main/web/desktop.scss b/src/main/web/desktop.scss index c5e03bb5..5a05fe0b 100644 --- a/src/main/web/desktop.scss +++ b/src/main/web/desktop.scss @@ -1140,6 +1140,9 @@ select{ .curPage{ font-weight: bold; } + .loader{ + margin-right: 5px; + } } .summaryWrap .pagination{ @@ -2911,3 +2914,72 @@ h2, h3, h4{ background: none; padding: 0; } + +#mailHistoryWrap{ + padding: 0; + border-top: solid 1px $wallPostSeparator; + >.loader{ + height: 49px; + } +} + +#mailShowHistory{ + display: block; + text-align: center; + padding: 18px 10px; +} + +.mailHistory{ + width: 465px; + margin: auto; + padding-bottom: 10px; + .summaryWrap{ + padding-left: 0; + padding-right: 0; + margin-bottom: 5px; + } + .bottomSummaryWrap{ + padding: 0; + margin-top: 5px; + } + .messageRow{ + display: grid; + grid-template-columns: 1fr 300px 50px; + grid-gap: 10px; + padding: 5px; + &.unread{ + background: #fef8ee; + } + >*{ + min-width: 0; + } + >.name{ + text-align: right; + font-weight: bold; + a{ + display: block; + } + &.self a{ + color: #a4ae46; + } + } + >.content{ + line-height: 130%; + p:first-child{ + margin-top: 0; + } + p:last-child{ + margin-bottom: 0; + } + .postAttachments .aspectWrapper .pseudoImage{ + max-width: 200px; + } + } + >.time{ + text-align: right; + a{ + color: #999; + } + } + } +}