From 2242ae59dbf00b77fe97cb82eadfeff014326c78 Mon Sep 17 00:00:00 2001 From: Grishka Date: Fri, 29 Oct 2021 21:54:46 +0300 Subject: [PATCH 01/65] Update to Java 17 LTS --- Dockerfile | 6 ++---- pom.xml | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index b208f523..106a0c55 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,11 @@ -FROM maven:3.6.3-openjdk-15 as builder +FROM maven:3.8.3-eclipse-temurin-17 as builder WORKDIR /usr/src/app -COPY pom.xml . -RUN mvn dependency:go-offline COPY . . RUN mvn package RUN java LibVipsDownloader.java -FROM openjdk:15-buster +FROM eclipse-temurin:17-jdk SHELL ["bash", "-c"] RUN mkdir -p /opt/smithereen diff --git a/pom.xml b/pom.xml index 92e3a212..12fcbeaf 100644 --- a/pom.xml +++ b/pom.xml @@ -6,8 +6,8 @@ UTF-8 UTF-8 - 15 - 15 + 17 + 17 me.grishka.smithereen From f8c8db620bae7707b7c39a33444732b681a80391 Mon Sep 17 00:00:00 2001 From: Grishka Date: Fri, 29 Oct 2021 21:57:40 +0300 Subject: [PATCH 02/65] Update CI --- .github/workflows/maven.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index e3a16a4c..69f6b4d7 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -9,10 +9,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 15 + - name: Set up JDK 17 uses: actions/setup-java@v1 with: - java-version: 15 + java-version: 17 - name: Cache Maven dependencies uses: actions/cache@v1 with: From b24d03c89cb38bc4fdd88df7ee86430842fc1a64 Mon Sep 17 00:00:00 2001 From: Grishka Date: Sun, 14 Nov 2021 17:27:27 +0300 Subject: [PATCH 03/65] fix --- src/main/resources/templates/email/reset_password.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/templates/email/reset_password.twig b/src/main/resources/templates/email/reset_password.twig index 06c31803..7192bf0e 100644 --- a/src/main/resources/templates/email/reset_password.twig +++ b/src/main/resources/templates/email/reset_password.twig @@ -4,6 +4,6 @@
{{ L('reset_password') }}
- {{ LG('email_password_reset_after', {'gender': gender}) }} + {{ L('email_password_reset_after', {'gender': gender}) }} {% endblock %} From b95ae2e3476a7bcb813a6d8322d365966b58f7f7 Mon Sep 17 00:00:00 2001 From: Grishka Date: Thu, 18 Nov 2021 00:18:34 +0300 Subject: [PATCH 04/65] Maybe fix federation with Lemmy --- .../smithereen/activitypub/objects/ActivityPubObject.java | 2 +- src/main/java/smithereen/routes/ActivityPubRoutes.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/smithereen/activitypub/objects/ActivityPubObject.java b/src/main/java/smithereen/activitypub/objects/ActivityPubObject.java index 808a8535..70354b2a 100644 --- a/src/main/java/smithereen/activitypub/objects/ActivityPubObject.java +++ b/src/main/java/smithereen/activitypub/objects/ActivityPubObject.java @@ -128,7 +128,7 @@ public JsonObject asActivityPubObject(JsonObject obj, ContextCollector contextCo if(summary!=null) obj.addProperty("summary", summary); if(tag!=null && !tag.isEmpty()) - obj.add("tag", serializeObjectArrayCompact(tag, contextCollector)); + obj.add("tag", serializeObjectArray(tag, contextCollector)); if(updated!=null) obj.addProperty("updated", Utils.formatDateAsISO(updated)); if(url!=null) diff --git a/src/main/java/smithereen/routes/ActivityPubRoutes.java b/src/main/java/smithereen/routes/ActivityPubRoutes.java index 38e0a361..72814fe1 100644 --- a/src/main/java/smithereen/routes/ActivityPubRoutes.java +++ b/src/main/java/smithereen/routes/ActivityPubRoutes.java @@ -818,13 +818,13 @@ private static Actor verifyHttpSignature(Request req, Actor userHint) throws Par throw new BadRequestException("Signature header has invalid format"); Map supportedSig=null; for(Map sig:values){ - if("rsa-sha256".equalsIgnoreCase(sig.get("algorithm"))){ + if("rsa-sha256".equalsIgnoreCase(sig.get("algorithm")) || "hs2019".equalsIgnoreCase(sig.get("algorithm"))){ supportedSig=sig; break; } } if(supportedSig==null) - throw new BadRequestException("Unsupported signature algorithm \""+values.get(0).get("algorithm")+"\", expected \"rsa-sha256\""); + throw new BadRequestException("Unsupported signature algorithm \""+values.get(0).get("algorithm")+"\", expected \"rsa-sha256\" or \"hs2019\""); if(!supportedSig.containsKey("keyId")) throw new BadRequestException("Signature header is missing keyId field"); From ccef5b05ed38b8882d2dbdd6138a2c74071ba8b1 Mon Sep 17 00:00:00 2001 From: Grishka Date: Sat, 20 Nov 2021 18:22:47 +0300 Subject: [PATCH 05/65] Refactor all the things! --- schema.sql | 12 +- .../java/smithereen/ApplicationContext.java | 28 +++ .../java/smithereen/ObjectLinkResolver.java | 12 +- .../smithereen/SmithereenApplication.java | 17 +- src/main/java/smithereen/Utils.java | 37 ++-- .../activitypub/ActivityPubWorker.java | 5 +- .../handlers/AnnounceNoteHandler.java | 2 +- .../objects/ActivityPubObject.java | 11 +- .../smithereen/activitypub/objects/Actor.java | 2 +- .../activitypub/objects/Tombstone.java | 3 +- .../controllers/FriendsController.java | 60 ++++++ .../controllers/GroupsController.java | 99 +++++++++ .../UserInteractionsController.java | 33 +++ .../controllers/UsersController.java | 38 ++++ .../controllers/WallController.java | 91 +++++++- src/main/java/smithereen/data/Invitation.java | 6 +- .../java/smithereen/data/ListAndTotal.java | 13 -- .../java/smithereen/data/PaginatedList.java | 28 +++ src/main/java/smithereen/data/Poll.java | 11 +- src/main/java/smithereen/data/Post.java | 10 +- .../java/smithereen/data/UserPermissions.java | 2 +- .../data/notifications/Notification.java | 7 +- .../jsonld/LinkedDataSignatures.java | 3 +- src/main/java/smithereen/lang/Lang.java | 7 +- .../smithereen/routes/ActivityPubRoutes.java | 6 +- .../java/smithereen/routes/GroupsRoutes.java | 126 +++++------ .../routes/NotificationsRoutes.java | 23 +- .../java/smithereen/routes/PostRoutes.java | 204 +++++------------- .../java/smithereen/routes/ProfileRoutes.java | 153 +++++++------ .../ActivityPubCollectionPageResponse.java | 6 +- .../storage/DatabaseSchemaUpdater.java | 6 +- .../smithereen/storage/DatabaseUtils.java | 14 ++ .../java/smithereen/storage/GroupStorage.java | 86 ++++++-- .../java/smithereen/storage/LikeStorage.java | 6 +- .../storage/NotificationsStorage.java | 7 +- .../java/smithereen/storage/PostStorage.java | 41 ++-- .../smithereen/storage/SQLQueryBuilder.java | 11 +- .../java/smithereen/storage/UserStorage.java | 76 +++++-- .../templates/LangDateFunction.java | 13 +- .../templates/RenderedTemplateResponse.java | 35 ++- src/main/resources/langs/en.json | 20 +- src/main/resources/langs/ru.json | 20 +- .../templates/common/create_group.twig | 2 +- .../templates/common/friends_tabbar.twig | 4 +- .../resources/templates/common/left_menu.twig | 33 +++ .../templates/common/pagination.twig | 21 ++ .../templates/common/wall_tabbar.twig | 4 +- .../resources/templates/desktop/feed.twig | 6 +- .../templates/desktop/friend_requests.twig | 8 +- .../resources/templates/desktop/friends.twig | 43 +++- .../resources/templates/desktop/group.twig | 2 +- .../templates/desktop/group_edit_members.twig | 8 +- .../resources/templates/desktop/groups.twig | 8 +- .../templates/desktop/notifications.twig | 8 +- .../resources/templates/desktop/page.twig | 28 +-- .../templates/desktop/pagination.twig | 15 -- .../resources/templates/desktop/profile.twig | 6 +- .../templates/desktop/user_grid.twig | 12 +- .../templates/desktop/wall_page.twig | 12 +- .../templates/desktop/wall_post.twig | 2 +- .../templates/desktop/wall_profile_block.twig | 4 +- src/main/resources/templates/mobile/feed.twig | 2 +- .../templates/mobile/friend_requests.twig | 5 +- .../resources/templates/mobile/friends.twig | 17 +- .../resources/templates/mobile/group.twig | 4 +- .../templates/mobile/group_edit_members.twig | 4 +- .../resources/templates/mobile/groups.twig | 3 +- .../templates/mobile/notifications.twig | 8 +- src/main/resources/templates/mobile/page.twig | 35 +-- .../templates/mobile/pagination.twig | 15 -- .../resources/templates/mobile/profile.twig | 4 +- .../resources/templates/mobile/user_grid.twig | 6 +- .../resources/templates/mobile/wall_page.twig | 4 +- .../mobile/wall_post_standalone.twig | 2 +- .../templates/mobile/wall_profile_block.twig | 6 +- src/main/web/common.scss | 1 - src/main/web/desktop.scss | 4 + src/main/web/mobile.scss | 22 +- 78 files changed, 1081 insertions(+), 677 deletions(-) create mode 100644 src/main/java/smithereen/controllers/FriendsController.java create mode 100644 src/main/java/smithereen/controllers/GroupsController.java create mode 100644 src/main/java/smithereen/controllers/UserInteractionsController.java create mode 100644 src/main/java/smithereen/controllers/UsersController.java delete mode 100644 src/main/java/smithereen/data/ListAndTotal.java create mode 100644 src/main/java/smithereen/data/PaginatedList.java create mode 100644 src/main/resources/templates/common/left_menu.twig create mode 100644 src/main/resources/templates/common/pagination.twig delete mode 100644 src/main/resources/templates/desktop/pagination.twig delete mode 100644 src/main/resources/templates/mobile/pagination.twig diff --git a/schema.sql b/schema.sql index b96ab47e..43feae43 100644 --- a/schema.sql +++ b/schema.sql @@ -1,13 +1,13 @@ # ************************************************************ # Sequel Ace SQL dump -# Version 3041 +# Версия 3043 # # https://sequel-ace.com/ # https://github.com/Sequel-Ace/Sequel-Ace # -# Host: 127.0.0.1 (MySQL 5.7.9) -# Database: smithereen -# Generation Time: 2021-10-20 17:22:10 +0000 +# Хост: 127.0.0.1 (MySQL 5.7.9) +# База данных: smithereen +# Generation Time: 2021-11-20 11:18:32 +0000 # ************************************************************ @@ -159,9 +159,11 @@ CREATE TABLE `followings` ( # ------------------------------------------------------------ CREATE TABLE `friend_requests` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `from_user_id` int(11) unsigned NOT NULL, `to_user_id` int(11) unsigned NOT NULL, `message` text, + PRIMARY KEY (`id`), UNIQUE KEY `from_user_id` (`from_user_id`,`to_user_id`), KEY `to_user_id` (`to_user_id`), CONSTRAINT `friend_requests_ibfk_1` FOREIGN KEY (`from_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, @@ -221,6 +223,7 @@ CREATE TABLE `groups` ( `private_key` blob, `avatar` text, `about` text, + `about_source` text, `profile_fields` text, `event_start_time` timestamp NULL DEFAULT NULL, `event_end_time` timestamp NULL DEFAULT NULL, @@ -458,6 +461,7 @@ CREATE TABLE `users` ( `ap_outbox` varchar(300) DEFAULT NULL, `ap_shared_inbox` varchar(300) DEFAULT NULL, `about` text, + `about_source` text, `gender` tinyint(4) unsigned NOT NULL DEFAULT '0', `profile_fields` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin, `avatar` text, diff --git a/src/main/java/smithereen/ApplicationContext.java b/src/main/java/smithereen/ApplicationContext.java index c06be5ff..db3975f0 100644 --- a/src/main/java/smithereen/ApplicationContext.java +++ b/src/main/java/smithereen/ApplicationContext.java @@ -1,15 +1,43 @@ package smithereen; +import smithereen.controllers.FriendsController; +import smithereen.controllers.GroupsController; +import smithereen.controllers.UserInteractionsController; +import smithereen.controllers.UsersController; import smithereen.controllers.WallController; public class ApplicationContext{ private final WallController wallController; + private final GroupsController groupsController; + private final UsersController usersController; + private final UserInteractionsController userInteractionsController; + private final FriendsController friendsController; public ApplicationContext(){ wallController=new WallController(this); + groupsController=new GroupsController(this); + usersController=new UsersController(this); + userInteractionsController=new UserInteractionsController(this); + friendsController=new FriendsController(this); } public WallController getWallController(){ return wallController; } + + public GroupsController getGroupsController(){ + return groupsController; + } + + public UsersController getUsersController(){ + return usersController; + } + + public UserInteractionsController getUserInteractionsController(){ + return userInteractionsController; + } + + public FriendsController getFriendsController(){ + return friendsController; + } } diff --git a/src/main/java/smithereen/ObjectLinkResolver.java b/src/main/java/smithereen/ObjectLinkResolver.java index 1f01151f..3d237239 100644 --- a/src/main/java/smithereen/ObjectLinkResolver.java +++ b/src/main/java/smithereen/ObjectLinkResolver.java @@ -128,12 +128,12 @@ public static T resolve(URI _link, Class expect public static void storeOrUpdateRemoteObject(ActivityPubObject o) throws SQLException{ o.storeDependencies(); - if(o instanceof ForeignUser) - UserStorage.putOrUpdateForeignUser((ForeignUser) o); - else if(o instanceof ForeignGroup) - GroupStorage.putOrUpdateForeignGroup((ForeignGroup) o); - else if(o instanceof Post) - PostStorage.putForeignWallPost((Post) o); + if(o instanceof ForeignUser fu) + UserStorage.putOrUpdateForeignUser(fu); + else if(o instanceof ForeignGroup fg) + GroupStorage.putOrUpdateForeignGroup(fg); + else if(o instanceof Post p) + PostStorage.putForeignWallPost(p); } private static T ensureTypeAndCast(ActivityPubObject obj, Class type){ diff --git a/src/main/java/smithereen/SmithereenApplication.java b/src/main/java/smithereen/SmithereenApplication.java index ce523595..19cb0479 100644 --- a/src/main/java/smithereen/SmithereenApplication.java +++ b/src/main/java/smithereen/SmithereenApplication.java @@ -283,6 +283,13 @@ public static void main(String[] args){ get("", ProfileRoutes::friends); getLoggedIn("/mutual", ProfileRoutes::mutualFriends); }); + path("/wall", ()->{ + get("", PostRoutes::userWallAll); + get("/own", PostRoutes::userWallOwn); + get("/with/:otherUserID", PostRoutes::wallToWall); + }); + get("/followers", ProfileRoutes::followers); + get("/following", ProfileRoutes::following); }); path("/groups/:id", ()->{ @@ -333,6 +340,9 @@ public static void main(String[] args){ get("/members", GroupsRoutes::members); get("/admins", GroupsRoutes::admins); + path("/wall", ()->{ + get("", PostRoutes::groupWall); + }); }); path("/posts/:postID", ()->{ @@ -402,13 +412,6 @@ public static void main(String[] args){ getWithCSRF("/respondToFriendRequest", ProfileRoutes::respondToFriendRequest); postWithCSRF("/doRemoveFriend", ProfileRoutes::doRemoveFriend); getLoggedIn("/confirmRemoveFriend", ProfileRoutes::confirmRemoveFriend); - get("/followers", ProfileRoutes::followers); - get("/following", ProfileRoutes::following); - path("/wall", ()->{ - get("", PostRoutes::wallAll); - get("/own", PostRoutes::wallOwn); - get("/with/:other_username", PostRoutes::wallToWall); - }); }); diff --git a/src/main/java/smithereen/Utils.java b/src/main/java/smithereen/Utils.java index 2c0346f1..6c736045 100644 --- a/src/main/java/smithereen/Utils.java +++ b/src/main/java/smithereen/Utils.java @@ -78,7 +78,6 @@ public class Utils{ private static final List RESERVED_USERNAMES=Arrays.asList("account", "settings", "feed", "activitypub", "api", "system", "users", "groups", "posts", "session", "robots.txt", "my", "activitypub_service_actor", "healthz"); private static final Whitelist HTML_SANITIZER=new MicroFormatAwareHTMLWhitelist(); - private static final ThreadLocal ISO_DATE_FORMAT=new ThreadLocal<>(); public static final String staticFileHash; private static Unidecode unidecode=Unidecode.toAscii(); private static Random rand=new Random(); @@ -162,6 +161,15 @@ public static int parseIntOrDefault(String s, int d){ } } + /** + * Parse a decimal integer from string without throwing any exceptions. + * @param s The string to parse. + * @return The parsed integer if successful, 0 on failure or if s was null. + */ + public static int safeParseInt(String s){ + return parseIntOrDefault(s, 0); + } + public static Object wrapError(Request req, Response resp, String errorKey){ return wrapError(req, resp, errorKey, null); } @@ -234,7 +242,7 @@ public static boolean isValidUsername(String username){ } public static boolean isReservedUsername(String username){ - return RESERVED_USERNAMES.contains(username.toLowerCase()); + return RESERVED_USERNAMES.contains(username.toLowerCase()) || username.toLowerCase().matches("^(id|club|event)\\d+$"); } public static boolean isValidEmail(String email){ @@ -269,23 +277,13 @@ public static String sanitizeHTML(String src, URI documentLocation){ return Jsoup.clean(src, documentLocation.toString(), HTML_SANITIZER); } - private static SimpleDateFormat isoDateFormat(){ - SimpleDateFormat format=ISO_DATE_FORMAT.get(); - if(format!=null) - return format; - format=new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); - format.setTimeZone(TimeZone.getTimeZone("GMT")); - ISO_DATE_FORMAT.set(format); - return format; - } - - public static String formatDateAsISO(Date date){ - return isoDateFormat().format(date); + public static String formatDateAsISO(Instant date){ + return DateTimeFormatter.ISO_INSTANT.format(date); } - public static Date parseISODate(String date){ + public static Instant parseISODate(String date){ try{ - return new Date(DateTimeFormatter.ISO_INSTANT.parse(date).getLong(ChronoField.INSTANT_SECONDS)*1000L); + return DateTimeFormatter.ISO_INSTANT.parse(date, Instant::from); }catch(DateTimeParseException x){ return null; } @@ -757,6 +755,13 @@ public static String convertIdnToAsciiIfNeeded(String domain) throws IllegalArgu return domain; } + public static int offset(Request req){ + String offset=req.queryParams("offset"); + if(StringUtils.isEmpty(offset)) + return 0; + return parseIntOrDefault(offset, 0); + } + @NotNull public static ApplicationContext context(Request req){ ApplicationContext context=req.attribute("context"); diff --git a/src/main/java/smithereen/activitypub/ActivityPubWorker.java b/src/main/java/smithereen/activitypub/ActivityPubWorker.java index 61d5f05a..f273afef 100644 --- a/src/main/java/smithereen/activitypub/ActivityPubWorker.java +++ b/src/main/java/smithereen/activitypub/ActivityPubWorker.java @@ -5,6 +5,7 @@ import java.net.URI; import java.sql.SQLException; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -256,7 +257,7 @@ else if(post.isGroupOwner()) delete.actor=new LinkOrObject(actor.activityPubID); delete.to=post.to; delete.cc=post.cc; - delete.published=new Date(); + delete.published=Instant.now(); delete.activityPubID=new UriBuilder(post.activityPubID).appendPath("delete").build(); sendActivityForPost(post, delete, actor); } @@ -530,7 +531,7 @@ public void sendPollVotes(User self, Poll poll, Actor pollOwner, List thread){ } private void doHandle(Post post) throws SQLException{ - long time=activity.published==null ? System.currentTimeMillis() : activity.published.getTime(); + long time=activity.published==null ? System.currentTimeMillis() : activity.published.toEpochMilli(); NewsfeedStorage.putEntry(actor.id, post.id, NewsfeedEntry.Type.RETOOT, new Timestamp(time)); if(!(post.user instanceof ForeignUser)){ diff --git a/src/main/java/smithereen/activitypub/objects/ActivityPubObject.java b/src/main/java/smithereen/activitypub/objects/ActivityPubObject.java index 70354b2a..52d28f7f 100644 --- a/src/main/java/smithereen/activitypub/objects/ActivityPubObject.java +++ b/src/main/java/smithereen/activitypub/objects/ActivityPubObject.java @@ -11,6 +11,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.sql.SQLException; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -54,19 +55,19 @@ public abstract class ActivityPubObject{ public String content; public URI context; public String name; - public Date endTime; + public Instant endTime; public LinkOrObject generator; public List image; public List icon; public URI inReplyTo; public LinkOrObject location; public LinkOrObject preview; - public Date published; + public Instant published; public LinkOrObject replies; - public Date startTime; + public Instant startTime; public String summary; public List tag; - public Date updated; + public Instant updated; public URI url; public List to; public List bto; @@ -204,7 +205,7 @@ protected URI tryParseURL(String url){ } } - protected Date tryParseDate(String date){ + protected Instant tryParseDate(String date){ if(date==null) return null; return Utils.parseISODate(date); diff --git a/src/main/java/smithereen/activitypub/objects/Actor.java b/src/main/java/smithereen/activitypub/objects/Actor.java index bce515f9..20b30fa7 100644 --- a/src/main/java/smithereen/activitypub/objects/Actor.java +++ b/src/main/java/smithereen/activitypub/objects/Actor.java @@ -173,7 +173,7 @@ protected ActivityPubObject parseActivityPubObject(JsonObject obj, ParserContext if(!keyOwner.equals(activityPubID)) throw new IllegalArgumentException("Key owner ("+keyOwner+") is not equal to user ID ("+activityPubID+")"); String pkeyEncoded=pkey.get("publicKeyPem").getAsString(); - pkeyEncoded=pkeyEncoded.replaceAll("-----(BEGIN|END) (RSA )?PUBLIC KEY-----", "").replace("\n", "").trim(); + pkeyEncoded=pkeyEncoded.replaceAll("-----(BEGIN|END) (RSA )?PUBLIC KEY-----", "").replaceAll("[^A-Za-z0-9+/=]", "").trim(); byte[] key=Base64.getDecoder().decode(pkeyEncoded); try{ X509EncodedKeySpec spec=new X509EncodedKeySpec(key); diff --git a/src/main/java/smithereen/activitypub/objects/Tombstone.java b/src/main/java/smithereen/activitypub/objects/Tombstone.java index bb488f3b..21bbc015 100644 --- a/src/main/java/smithereen/activitypub/objects/Tombstone.java +++ b/src/main/java/smithereen/activitypub/objects/Tombstone.java @@ -2,6 +2,7 @@ import com.google.gson.JsonObject; +import java.time.Instant; import java.util.Date; import smithereen.Utils; @@ -11,7 +12,7 @@ public class Tombstone extends ActivityPubObject{ public String formerType; - public Date deleted; + public Instant deleted; @Override public String getType(){ diff --git a/src/main/java/smithereen/controllers/FriendsController.java b/src/main/java/smithereen/controllers/FriendsController.java new file mode 100644 index 00000000..2360310b --- /dev/null +++ b/src/main/java/smithereen/controllers/FriendsController.java @@ -0,0 +1,60 @@ +package smithereen.controllers; + +import java.sql.SQLException; + +import smithereen.ApplicationContext; +import smithereen.data.FriendRequest; +import smithereen.data.PaginatedList; +import smithereen.data.User; +import smithereen.exceptions.InternalServerErrorException; +import smithereen.storage.UserStorage; + +public class FriendsController{ + private final ApplicationContext context; + + public FriendsController(ApplicationContext context){ + this.context=context; + } + + public PaginatedList getIncomingFriendRequests(User self, int offset, int count){ + try{ + return UserStorage.getIncomingFriendRequestsForUser(self.id, offset, count); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public PaginatedList getFollowers(User user, int offset, int count){ + try{ + return UserStorage.getNonMutualFollowers(user.id, true, true, offset, count); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public PaginatedList getFollows(User user, int offset, int count){ + try{ + return UserStorage.getNonMutualFollowers(user.id, false, true, offset, count); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public PaginatedList getFriends(User user, int offset, int count){ + try{ + return UserStorage.getFriendListForUser(user.id, offset, count); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public PaginatedList getMutualFriends(User user, User otherUser, int offset, int count){ + try{ + if(user.id==otherUser.id) + throw new IllegalArgumentException("must be different users"); + return UserStorage.getMutualFriendListForUser(user.id, otherUser.id, offset, count); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } +} diff --git a/src/main/java/smithereen/controllers/GroupsController.java b/src/main/java/smithereen/controllers/GroupsController.java new file mode 100644 index 00000000..f33d2567 --- /dev/null +++ b/src/main/java/smithereen/controllers/GroupsController.java @@ -0,0 +1,99 @@ +package smithereen.controllers; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.SQLException; +import java.time.Instant; +import java.util.List; +import java.util.Objects; + +import smithereen.ApplicationContext; +import smithereen.Utils; +import smithereen.activitypub.ActivityPubWorker; +import smithereen.data.ForeignGroup; +import smithereen.data.Group; +import smithereen.data.PaginatedList; +import smithereen.data.User; +import smithereen.exceptions.InternalServerErrorException; +import smithereen.exceptions.ObjectNotFoundException; +import smithereen.storage.GroupStorage; +import spark.utils.StringUtils; + +public class GroupsController{ + private static final Logger LOG=LoggerFactory.getLogger(GroupsController.class); + + private final ApplicationContext context; + + public GroupsController(ApplicationContext context){ + this.context=context; + } + + public Group createGroup(@NotNull User admin, @NotNull String name, @Nullable String description){ + return createGroupInternal(admin, name, description, false, null, null); + } + + public Group createEvent(@NotNull User admin, @NotNull String name, @Nullable String description, @NotNull Instant startTime, @Nullable Instant endTime){ + return createGroupInternal(admin, name, description, true, startTime, endTime); + } + + @NotNull + private Group createGroupInternal(User admin, String name, String description, boolean isEvent, Instant startTime, Instant endTime){ + try{ + if(StringUtils.isEmpty(name)) + throw new IllegalArgumentException("name is empty"); + int id=GroupStorage.createGroup(name, Utils.preprocessPostHTML(description, null), description, admin.id, false); + Group group=Objects.requireNonNull(GroupStorage.getById(id)); + ActivityPubWorker.getInstance().sendAddToGroupsCollectionActivity(admin, group); + return group; + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public PaginatedList getUserGroups(@NotNull User user, int offset, int count){ + try{ + return GroupStorage.getUserGroups(user.id, offset, count); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public PaginatedList getUserManagedGroups(@NotNull User user, int offset, int count){ + try{ + return GroupStorage.getUserManagedGroups(user.id, offset, count); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public Group getGroupOrThrow(int id){ + try{ + if(id<=0) + throw new ObjectNotFoundException("err_group_not_found"); + Group group=GroupStorage.getById(id); + if(group==null) + throw new ObjectNotFoundException("err_group_not_found"); + return group; + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public Group getLocalGroupOrThrow(int id){ + Group group=getGroupOrThrow(id); + if(group instanceof ForeignGroup) + throw new ObjectNotFoundException("err_group_not_found"); + return group; + } + + public PaginatedList getMembers(Group group, int offset, int count){ + try{ + return GroupStorage.getMembers(group.id, offset, count); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } +} diff --git a/src/main/java/smithereen/controllers/UserInteractionsController.java b/src/main/java/smithereen/controllers/UserInteractionsController.java new file mode 100644 index 00000000..c0a89c98 --- /dev/null +++ b/src/main/java/smithereen/controllers/UserInteractionsController.java @@ -0,0 +1,33 @@ +package smithereen.controllers; + +import java.sql.SQLException; +import java.util.Collections; +import java.util.List; + +import smithereen.ApplicationContext; +import smithereen.data.PaginatedList; +import smithereen.data.Post; +import smithereen.data.User; +import smithereen.data.UserInteractions; +import smithereen.exceptions.InternalServerErrorException; +import smithereen.storage.LikeStorage; +import smithereen.storage.PostStorage; +import smithereen.storage.UserStorage; + +public class UserInteractionsController{ + private final ApplicationContext context; + + public UserInteractionsController(ApplicationContext context){ + this.context=context; + } + + public PaginatedList getLikesForObject(Post object, User self, int offset, int count){ + try{ + UserInteractions interactions=PostStorage.getPostInteractions(Collections.singletonList(object.id), 0).get(object.id); + List users=UserStorage.getByIdAsList(LikeStorage.getPostLikes(object.id, 0, offset, count)); + return new PaginatedList<>(users, interactions.likeCount, offset, count); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } +} diff --git a/src/main/java/smithereen/controllers/UsersController.java b/src/main/java/smithereen/controllers/UsersController.java new file mode 100644 index 00000000..6d42701c --- /dev/null +++ b/src/main/java/smithereen/controllers/UsersController.java @@ -0,0 +1,38 @@ +package smithereen.controllers; + +import java.sql.SQLException; + +import smithereen.ApplicationContext; +import smithereen.data.ForeignUser; +import smithereen.data.User; +import smithereen.exceptions.InternalServerErrorException; +import smithereen.exceptions.ObjectNotFoundException; +import smithereen.storage.UserStorage; + +public class UsersController{ + private final ApplicationContext context; + + public UsersController(ApplicationContext context){ + this.context=context; + } + + public User getUserOrThrow(int id){ + try{ + if(id<=0) + throw new ObjectNotFoundException("err_user_not_found"); + User user=UserStorage.getById(id); + if(user==null) + throw new ObjectNotFoundException("err_user_not_found"); + return user; + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public User getLocalUserOrThrow(int id){ + User user=getUserOrThrow(id); + if(user instanceof ForeignUser) + throw new ObjectNotFoundException("err_user_not_found"); + return user; + } +} diff --git a/src/main/java/smithereen/controllers/WallController.java b/src/main/java/smithereen/controllers/WallController.java index ba8503ae..7c3fa551 100644 --- a/src/main/java/smithereen/controllers/WallController.java +++ b/src/main/java/smithereen/controllers/WallController.java @@ -13,8 +13,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import smithereen.ApplicationContext; @@ -27,10 +30,12 @@ import smithereen.activitypub.objects.LocalImage; import smithereen.data.ForeignUser; import smithereen.data.Group; +import smithereen.data.PaginatedList; import smithereen.data.Poll; import smithereen.data.PollOption; import smithereen.data.Post; import smithereen.data.User; +import smithereen.data.UserInteractions; import smithereen.data.UserPermissions; import smithereen.data.notifications.NotificationUtils; import smithereen.exceptions.BadRequestException; @@ -51,7 +56,7 @@ public class WallController{ private static final Logger LOG=LoggerFactory.getLogger(WallController.class); - private ApplicationContext context; + private final ApplicationContext context; public WallController(ApplicationContext context){ this.context=context; @@ -137,8 +142,8 @@ public Post createWallPost(@NotNull User author, @NotNull Actor wallOwner, int i if(text.length()==0 && StringUtils.isEmpty(attachments) && pollID==0) throw new BadRequestException("Empty post"); - int ownerUserID=wallOwner instanceof User ? ((User) wallOwner).id : 0; - int ownerGroupID=wallOwner instanceof Group ? ((Group) wallOwner).id : 0; + int ownerUserID=wallOwner instanceof User u ? u.id : 0; + int ownerGroupID=wallOwner instanceof Group g ? g.id : 0; int[] replyKey; if(parent!=null){ replyKey=new int[parent.replyKey.length+1]; @@ -203,8 +208,7 @@ public User resolveMention(String username, String domain){ } URI uri=ActivityPub.resolveUsername(username, domain); ActivityPubObject obj=ActivityPub.fetchRemoteObject(uri); - if(obj instanceof ForeignUser){ - ForeignUser _user=(ForeignUser)obj; + if(obj instanceof ForeignUser _user){ UserStorage.putOrUpdateForeignUser(_user); if(!mentionedUsers.contains(_user)) mentionedUsers.add(_user); @@ -314,8 +318,7 @@ public Post editPost(@NotNull UserPermissions permissions, int id, @NotNull Stri ArrayList remainingAttachments=new ArrayList<>(attachmentIDs); if(post.attachment!=null){ for(ActivityPubObject att:post.attachment){ - if(att instanceof LocalImage){ - LocalImage li=(LocalImage) att; + if(att instanceof LocalImage li){ if(!remainingAttachments.remove(li.localID)){ LOG.debug("Deleting attachment: {}", li.localID); MediaStorageUtils.deleteAttachmentFiles(li); @@ -363,4 +366,78 @@ public Post editPost(@NotNull UserPermissions permissions, int id, @NotNull Stri throw new InternalServerErrorException(x); } } + + /** + * Get posts from a wall. + * @param owner Wall owner, either a user or a group + * @param ownOnly Whether to return only user's own posts or include other's posts + * @param offset Pagination offset + * @param count Maximum number of posts to return + * @return A reverse-chronologically sorted paginated list of wall posts + */ + public PaginatedList getWallPosts(@NotNull Actor owner, boolean ownOnly, int offset, int count){ + try{ + int[] postCount={0}; + List wall=PostStorage.getWallPosts(owner.getLocalID(), owner instanceof Group, 0, 0, offset, count, postCount, ownOnly); + return new PaginatedList<>(wall, postCount[0], offset, count); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + /** + * Get posts that two users posted on each other's walls. + * @param user The first user + * @param otherUser The second user + * @param offset Pagination offset + * @param count Maximum number of posts to return + * @return A reverse-chronologically sorted paginated list of wall posts + */ + public PaginatedList getWallToWallPosts(@NotNull User user, @NotNull User otherUser, int offset, int count){ + try{ + int[] postCount={0}; + List wall=PostStorage.getWallToWall(user.id, otherUser.id, offset, count, postCount); + return new PaginatedList<>(wall, postCount[0], offset, count); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + /** + * Add top-level comments to each post. + * @param posts List of posts to add comments to + */ + public void populateCommentPreviews(@NotNull List posts){ + try{ + Set postIDs=posts.stream().map((Post p)->p.id).collect(Collectors.toSet()); + Map> allComments=PostStorage.getRepliesForFeed(postIDs); + for(Post post:posts){ + PaginatedList comments=allComments.get(post.id); + if(comments!=null){ + post.repliesObjects=comments.list; + post.totalTopLevelComments=comments.total; + } + } + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + /** + * Get {@link UserInteractions} for posts. + * @param posts List of posts to get user interactions for + * @param self Current user to check whether posts are liked + * @return A map from a post ID to a {@link UserInteractions} object for each post + */ + public Map getUserInteractions(@NotNull List posts, @Nullable User self){ + try{ + Set postIDs=posts.stream().map((Post p)->p.id).collect(Collectors.toSet()); + for(Post p:posts){ + p.getAllReplyIDs(postIDs); + } + return PostStorage.getPostInteractions(postIDs, self!=null ? self.id : 0); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } } diff --git a/src/main/java/smithereen/data/Invitation.java b/src/main/java/smithereen/data/Invitation.java index e6dbe277..042191d0 100644 --- a/src/main/java/smithereen/data/Invitation.java +++ b/src/main/java/smithereen/data/Invitation.java @@ -3,17 +3,19 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; +import java.time.Instant; import smithereen.Utils; +import smithereen.storage.DatabaseUtils; public class Invitation{ public String code; - public Timestamp createdAt; + public Instant createdAt; public static Invitation fromResultSet(ResultSet res) throws SQLException{ Invitation inv=new Invitation(); inv.code=Utils.byteArrayToHexString(res.getBytes("code")); - inv.createdAt=res.getTimestamp("created"); + inv.createdAt=DatabaseUtils.getInstant(res, "created"); return inv; } } diff --git a/src/main/java/smithereen/data/ListAndTotal.java b/src/main/java/smithereen/data/ListAndTotal.java deleted file mode 100644 index f75619d7..00000000 --- a/src/main/java/smithereen/data/ListAndTotal.java +++ /dev/null @@ -1,13 +0,0 @@ -package smithereen.data; - -import java.util.List; - -public class ListAndTotal{ - public List list; - public int total; - - public ListAndTotal(List list, int total){ - this.list=list; - this.total=total; - } -} diff --git a/src/main/java/smithereen/data/PaginatedList.java b/src/main/java/smithereen/data/PaginatedList.java new file mode 100644 index 00000000..5ffc9c5e --- /dev/null +++ b/src/main/java/smithereen/data/PaginatedList.java @@ -0,0 +1,28 @@ +package smithereen.data; + +import java.util.Collections; +import java.util.List; + +public class PaginatedList{ + public List list; + public int total; + + public transient int offset; + public transient int perPage; + + public PaginatedList(List list, int total){ + this.list=list; + this.total=total; + } + + public PaginatedList(List list, int total, int offset, int perPage){ + this.list=list; + this.total=total; + this.offset=offset; + this.perPage=perPage; + } + + public static PaginatedList emptyList(int perPage){ + return new PaginatedList<>(Collections.emptyList(), 0, 0, perPage); + } +} diff --git a/src/main/java/smithereen/data/Poll.java b/src/main/java/smithereen/data/Poll.java index dd390227..e05b7575 100644 --- a/src/main/java/smithereen/data/Poll.java +++ b/src/main/java/smithereen/data/Poll.java @@ -4,11 +4,14 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; +import java.time.Instant; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Objects; +import smithereen.storage.DatabaseUtils; + public class Poll{ public int id; public int ownerID; @@ -16,7 +19,7 @@ public class Poll{ public boolean multipleChoice; public boolean anonymous; public List options; - public Date endTime; + public Instant endTime; public int numVoters; public URI activityPubID; @@ -27,9 +30,7 @@ public static Poll fromResultSet(ResultSet res) throws SQLException{ p.question=res.getString("question"); p.multipleChoice=res.getBoolean("is_multi_choice"); p.anonymous=res.getBoolean("is_anonymous"); - Timestamp end=res.getTimestamp("end_time"); - if(end!=null) - p.endTime=new Date(end.getTime()); + p.endTime=DatabaseUtils.getInstant(res, "end_time"); p.numVoters=res.getInt("num_voted_users"); p.options=new ArrayList<>(); String apID=res.getString("ap_id"); @@ -69,7 +70,7 @@ public String toString(){ } public boolean isExpired(){ - return endTime!=null && endTime.getTime() likes=LikeStorage.getLikes(post.id, post.activityPubID, Like.ObjectType.POST, offset, count); + PaginatedList likes=LikeStorage.getLikes(post.id, post.activityPubID, Like.ObjectType.POST, offset, count); return ActivityPubCollectionPageResponse.forObjects(likes).ordered(); } @@ -324,7 +324,7 @@ public static Object userOutbox(Request req, Response resp) throws SQLException{ int minID=Math.max(0, _minID); int maxID=Math.max(0, _maxID); int[] _total={0}; - List posts=PostStorage.getWallPosts(user.id, false, minID, maxID, 0, _total, true); + List posts=PostStorage.getWallPosts(user.id, false, minID, maxID, 0, 25, _total, true); int total=_total[0]; CollectionPage page=new CollectionPage(true); page.totalItems=total; diff --git a/src/main/java/smithereen/routes/GroupsRoutes.java b/src/main/java/smithereen/routes/GroupsRoutes.java index 5768289a..4c674d0e 100644 --- a/src/main/java/smithereen/routes/GroupsRoutes.java +++ b/src/main/java/smithereen/routes/GroupsRoutes.java @@ -15,7 +15,7 @@ import java.util.stream.Collectors; import smithereen.data.ForeignUser; -import smithereen.data.ListAndTotal; +import smithereen.data.PaginatedList; import smithereen.data.SizedImage; import smithereen.data.feed.NewsfeedEntry; import smithereen.exceptions.BadRequestException; @@ -34,7 +34,6 @@ import smithereen.data.WebDeltaResponse; import smithereen.exceptions.UserActionNotAllowedException; import smithereen.lang.Lang; -import smithereen.storage.DatabaseUtils; import smithereen.storage.GroupStorage; import smithereen.storage.NewsfeedStorage; import smithereen.storage.PostStorage; @@ -49,13 +48,9 @@ public class GroupsRoutes{ - private static Group getGroup(Request req) throws SQLException{ + private static Group getGroup(Request req){ int id=parseIntOrDefault(req.params(":id"), 0); - Group group=GroupStorage.getById(id); - if(group==null){ - throw new ObjectNotFoundException("err_group_not_found"); - } - return group; + return context(req).getGroupsController().getGroupOrThrow(id); } private static Group getGroupAndRequireLevel(Request req, Account self, Group.AdminLevel level) throws SQLException{ @@ -66,38 +61,34 @@ private static Group getGroupAndRequireLevel(Request req, Account self, Group.Ad return group; } - public static Object myGroups(Request req, Response resp, Account self) throws SQLException{ - jsLangKey(req, "cancel", "create"); - RenderedTemplateResponse model=new RenderedTemplateResponse("groups", req).with("tab", "groups").with("title", lang(req).get("groups")); - model.with("groups", GroupStorage.getUserGroups(self.user.id)); - model.with("owner", self.user); - return model; + public static Object myGroups(Request req, Response resp, Account self){ + return userGroups(req, resp, self.user); } - public static Object userGroups(Request req, Response resp) throws SQLException{ + public static Object userGroups(Request req, Response resp){ int uid=parseIntOrDefault(req.params(":id"), 0); - if(uid==0) - throw new ObjectNotFoundException("err_user_not_found"); - User user=UserStorage.getById(uid); - if(user==null) - throw new ObjectNotFoundException("err_user_not_found"); + User user=context(req).getUsersController().getUserOrThrow(uid); + return userGroups(req, resp, user); + } + + public static Object userGroups(Request req, Response resp, User user){ jsLangKey(req, "cancel", "create"); RenderedTemplateResponse model=new RenderedTemplateResponse("groups", req).with("tab", "groups").with("title", lang(req).get("groups")); - model.with("groups", GroupStorage.getUserGroups(user.id)); + model.paginate(context(req).getGroupsController().getUserGroups(user, offset(req), 100)); model.with("owner", user); return model; } - public static Object myManagedGroups(Request req, Response resp, Account self) throws SQLException{ + public static Object myManagedGroups(Request req, Response resp, Account self){ jsLangKey(req, "cancel", "create"); RenderedTemplateResponse model=new RenderedTemplateResponse("groups", req).with("tab", "managed").with("title", lang(req).get("groups")); - model.with("groups", GroupStorage.getUserManagedGroups(self.user.id)).with("owner", self.user); + model.paginate(context(req).getGroupsController().getUserManagedGroups(self.user, offset(req), 100)).with("owner", self.user); return model; } - public static Object createGroup(Request req, Response resp, Account self) throws SQLException{ + public static Object createGroup(Request req, Response resp, Account self){ RenderedTemplateResponse model=new RenderedTemplateResponse("create_group", req); - return wrapForm(req, resp, "create_group", "/my/groups/create", lang(req).get("create_group"), "create", model); + return wrapForm(req, resp, "create_group", "/my/groups/create", lang(req).get("create_group_title"), "create", model); } private static Object groupCreateError(Request req, Response resp, String errKey){ @@ -106,62 +97,56 @@ private static Object groupCreateError(Request req, Response resp, String errKey } RenderedTemplateResponse model=new RenderedTemplateResponse("create_group", req); model.with("groupName", req.queryParams("name")).with("groupUsername", req.queryParams("username")); - return wrapForm(req, resp, "create_group", "/my/groups/create", lang(req).get("create_group"), "create", model); + return wrapForm(req, resp, "create_group", "/my/groups/create", lang(req).get("create_group_title"), "create", model); } - public static Object doCreateGroup(Request req, Response resp, Account self) throws SQLException{ - String username=req.queryParams("username"); + public static Object doCreateGroup(Request req, Response resp, Account self){ String name=req.queryParams("name"); - - if(!isValidUsername(username)) - return groupCreateError(req, resp, "err_group_invalid_username"); - if(isReservedUsername(username)) - return groupCreateError(req, resp, "err_group_reserved_username"); - - final int[] id={0}; - boolean r=DatabaseUtils.runWithUniqueUsername(username, ()->{ - id[0]=GroupStorage.createGroup(name, username, self.user.id); - }); - - if(r){ - ActivityPubWorker.getInstance().sendAddToGroupsCollectionActivity(self.user, GroupStorage.getById(id[0])); - if(isAjax(req)){ - return new WebDeltaResponse(resp).replaceLocation("/"+username); - }else{ - resp.redirect(Config.localURI("/"+username).toString()); - return ""; - } + String description=req.queryParams("description"); + String eventTime=req.queryParams("event_start_time"); + String eventDate=req.queryParams("event_start_date"); +// +// if(!isValidUsername(username)) +// return groupCreateError(req, resp, "err_group_invalid_username"); +// if(isReservedUsername(username)) +// return groupCreateError(req, resp, "err_group_reserved_username"); +// +// final int[] id={0}; +// boolean r=DatabaseUtils.runWithUniqueUsername(username, ()->{ +// id[0]=GroupStorage.createGroup(name, username, self.user.id); +// }); +// +// if(r){ +// ActivityPubWorker.getInstance().sendAddToGroupsCollectionActivity(self.user, GroupStorage.getById(id[0])); +// }else{ +// return groupCreateError(req, resp, "err_group_username_taken"); +// } + Group group=context(req).getGroupsController().createGroup(self.user, name, description); + if(isAjax(req)){ + return new WebDeltaResponse(resp).replaceLocation("/"+group.username); }else{ - return groupCreateError(req, resp, "err_group_username_taken"); + resp.redirect(Config.localURI("/"+group.username).toString()); + return ""; } } public static Object groupProfile(Request req, Response resp, Group group) throws SQLException{ - int pageOffset=parseIntOrDefault(req.queryParams("offset"), 0); SessionInfo info=Utils.sessionInfo(req); @Nullable Account self=info!=null ? info.account : null; List members=GroupStorage.getRandomMembersForProfile(group.id); - int[] totalPosts={0}; - List wall=PostStorage.getWallPosts(group.id, true, 0, 0, pageOffset, totalPosts, false); - Set postIDs=wall.stream().map((Post p)->p.id).collect(Collectors.toSet()); + int offset=offset(req); + PaginatedList wall=context(req).getWallController().getWallPosts(group, false, offset, 20); if(req.attribute("mobile")==null){ - Map> allComments=PostStorage.getRepliesForFeed(postIDs); - for(Post post:wall){ - ListAndTotal comments=allComments.get(post.id); - if(comments!=null){ - post.repliesObjects=comments.list; - post.totalTopLevelComments=comments.total; - post.getAllReplyIDs(postIDs); - } - } + context(req).getWallController().populateCommentPreviews(wall.list); } - HashMap interactions=PostStorage.getPostInteractions(postIDs, self!=null ? self.user.id : 0); + + Map interactions=context(req).getWallController().getUserInteractions(wall.list, self!=null ? self.user : null); Lang l=lang(req); RenderedTemplateResponse model=new RenderedTemplateResponse("group", req); - model.with("group", group).with("members", members).with("postCount", totalPosts[0]).with("pageOffset", pageOffset).with("wall", wall); + model.with("group", group).with("members", members).with("postCount", wall.total).paginate(wall); model.with("postInteractions", interactions); model.with("title", group.name); model.with("admins", GroupStorage.getGroupAdmins(group.id)); @@ -185,7 +170,7 @@ public static Object groupProfile(Request req, Response resp, Group group) throw meta.put("og:title", group.name); meta.put("og:url", group.url.toString()); meta.put("og:username", group.getFullUsername()); - String descr=l.get("X_members", Map.of("count", group.memberCount))+", "+l.get("X_posts", Map.of("count", totalPosts[0])); + String descr=l.get("X_members", Map.of("count", group.memberCount))+", "+l.get("X_posts", Map.of("count", wall.total)); if(StringUtils.isNotEmpty(group.summary)) descr+="\n"+Jsoup.clean(group.summary, Whitelist.none()); meta.put("og:description", descr); @@ -283,12 +268,10 @@ public static Object saveGeneral(Request req, Response resp, Account self) throw return ""; } - public static Object members(Request req, Response resp) throws SQLException{ + public static Object members(Request req, Response resp){ Group group=getGroup(req); - int offset=parseIntOrDefault(req.queryParams("offset"), 0); - ListAndTotal users=GroupStorage.getMembers(group.id, offset, 100); - RenderedTemplateResponse model=new RenderedTemplateResponse(isAjax(req) ? "user_grid" : "content_wrap", req).with("users", users.list); - model.with("pageOffset", offset).with("total", group.memberCount).with("paginationUrlPrefix", "/groups/"+group.id+"/members?offset="); + RenderedTemplateResponse model=new RenderedTemplateResponse(isAjax(req) ? "user_grid" : "content_wrap", req); + model.paginate(context(req).getGroupsController().getMembers(group, offset(req), 100)); model.with("summary", lang(req).get("summary_group_X_members", Map.of("count", group.memberCount))); // if(isAjax(req)){ // if(req.queryParams("fromPagination")==null) @@ -323,11 +306,8 @@ public static Object editMembers(Request req, Response resp, Account self) throw Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.MODERATOR); Group.AdminLevel level=GroupStorage.getGroupMemberAdminLevel(group.id, self.user.id); RenderedTemplateResponse model=new RenderedTemplateResponse("group_edit_members", req); - int offset=parseIntOrDefault(req.queryParams("offset"), 0); - List users=GroupStorage.getMembers(group.id, offset, 100).list; - model.with("pageOffset", offset).with("total", group.memberCount).with("paginationUrlPrefix", "/groups/"+group.id+"/editMembers?offset="); + model.paginate(context(req).getGroupsController().getMembers(group, offset(req), 100)); model.with("group", group).with("title", group.name); - model.with("members", users); model.with("adminIDs", GroupStorage.getGroupAdmins(group.id).stream().map(adm->adm.user.id).collect(Collectors.toList())); model.with("canAddAdmins", level.isAtLeast(Group.AdminLevel.ADMIN)); model.with("adminLevel", level); diff --git a/src/main/java/smithereen/routes/NotificationsRoutes.java b/src/main/java/smithereen/routes/NotificationsRoutes.java index f71aca79..dcc3fed3 100644 --- a/src/main/java/smithereen/routes/NotificationsRoutes.java +++ b/src/main/java/smithereen/routes/NotificationsRoutes.java @@ -3,10 +3,13 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import smithereen.Utils; import smithereen.data.Account; +import smithereen.data.PaginatedList; import smithereen.data.Post; import smithereen.data.User; import smithereen.data.notifications.Notification; @@ -22,12 +25,12 @@ public class NotificationsRoutes{ public static Object notifications(Request req, Response resp, Account self) throws SQLException{ - int offset=Utils.parseIntOrDefault(req.queryParams("offset"), 0); + int offset=offset(req); RenderedTemplateResponse model=new RenderedTemplateResponse("notifications", req); int[] total={0}; - List notifications=NotificationsStorage.getNotifications(self.user.id, offset, total); - model.with("title", lang(req).get("notifications")).with("notifications", notifications).with("offset", offset).with("total", total[0]); - ArrayList needUsers=new ArrayList<>(), needPosts=new ArrayList<>(); + List notifications=NotificationsStorage.getNotifications(self.user.id, offset, 50, total); + model.pageTitle(lang(req).get("notifications")).paginate(new PaginatedList(notifications, total[0], offset, 50)); + HashSet needUsers=new HashSet<>(), needPosts=new HashSet<>(); for(Notification n:notifications){ needUsers.add(n.actorID); @@ -39,17 +42,9 @@ public static Object notifications(Request req, Response resp, Account self) thr } } - HashMap users=new HashMap<>(); - HashMap posts=new HashMap<>(); - // TODO get all users & posts in one database query - for(Integer uid:needUsers){ - if(users.containsKey(uid)) - continue; - users.put(uid, UserStorage.getById(uid)); - } + Map users=UserStorage.getById(needUsers); + Map posts=new HashMap<>(); for(Integer pid:needPosts){ - if(posts.containsKey(pid)) - continue; posts.put(pid, PostStorage.getPostByID(pid, false)); } diff --git a/src/main/java/smithereen/routes/PostRoutes.java b/src/main/java/smithereen/routes/PostRoutes.java index d66d0220..2eaf88fe 100644 --- a/src/main/java/smithereen/routes/PostRoutes.java +++ b/src/main/java/smithereen/routes/PostRoutes.java @@ -1,7 +1,5 @@ package smithereen.routes; -import com.google.gson.JsonArray; - import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -10,10 +8,10 @@ import java.net.URI; import java.net.URLEncoder; import java.sql.SQLException; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -25,16 +23,12 @@ import smithereen.Config; import smithereen.Utils; -import smithereen.activitypub.ActivityPub; import smithereen.activitypub.ActivityPubWorker; -import smithereen.activitypub.objects.ActivityPubObject; import smithereen.activitypub.objects.Actor; -import smithereen.activitypub.objects.ForeignActor; -import smithereen.controllers.WallController; import smithereen.data.Account; import smithereen.data.ForeignUser; import smithereen.data.Group; -import smithereen.data.ListAndTotal; +import smithereen.data.PaginatedList; import smithereen.data.Poll; import smithereen.data.PollOption; import smithereen.data.Post; @@ -48,13 +42,11 @@ import smithereen.data.feed.NewsfeedEntry; import smithereen.data.feed.PostNewsfeedEntry; import smithereen.data.notifications.Notification; -import smithereen.data.notifications.NotificationUtils; import smithereen.exceptions.BadRequestException; import smithereen.exceptions.ObjectNotFoundException; import smithereen.exceptions.UserActionNotAllowedException; import smithereen.storage.GroupStorage; import smithereen.storage.LikeStorage; -import smithereen.storage.MediaCache; import smithereen.storage.MediaStorageUtils; import smithereen.storage.NotificationsStorage; import smithereen.storage.PostStorage; @@ -101,7 +93,7 @@ public static Object createWallPost(Request req, Response resp, Account self, @N if(timeLimit){ int seconds=parseIntOrDefault(req.queryParams("pollTimeLimitValue"), 0); if(seconds>60) - poll.endTime=new Date(System.currentTimeMillis()+(seconds*1000L)); + poll.endTime=Instant.now().plusSeconds(seconds); } poll.options=pollOptions.stream().map(o->{ PollOption opt=new PollOption(); @@ -202,7 +194,7 @@ public static Object editPost(Request req, Response resp, Account self) throws S if(timeLimit){ int seconds=parseIntOrDefault(req.queryParams("pollTimeLimitValue"), 0); if(seconds>60) - poll.endTime=new Date(System.currentTimeMillis()+(seconds*1000L)); + poll.endTime=Instant.now().plusSeconds(seconds); else if(seconds==-1 && post.poll!=null) poll.endTime=post.poll.endTime; } @@ -245,40 +237,17 @@ public static Object feed(Request req, Response resp, Account self) throws SQLEx int offset=parseIntOrDefault(req.queryParams("offset"), 0); int[] total={0}; List feed=PostStorage.getFeed(userID, startFromID, offset, total); - HashSet postIDs=new HashSet<>(); - for(NewsfeedEntry e:feed){ - if(e instanceof PostNewsfeedEntry){ - PostNewsfeedEntry pe=(PostNewsfeedEntry) e; - if(pe.post!=null){ - postIDs.add(pe.post.id); - }else{ - System.err.println("No post: "+pe); - } - } + List feedPosts=feed.stream().filter(e->e instanceof PostNewsfeedEntry pe && pe.post!=null).map(e->((PostNewsfeedEntry)e).post).collect(Collectors.toList()); + if(req.attribute("mobile")==null && !feedPosts.isEmpty()){ + context(req).getWallController().populateCommentPreviews(feedPosts); } - if(req.attribute("mobile")==null && !postIDs.isEmpty()){ - Map> allComments=PostStorage.getRepliesForFeed(postIDs); - for(NewsfeedEntry e:feed){ - if(e instanceof PostNewsfeedEntry){ - PostNewsfeedEntry pe=(PostNewsfeedEntry) e; - if(pe.post!=null){ - ListAndTotal comments=allComments.get(pe.post.id); - if(comments!=null){ - pe.post.repliesObjects=comments.list; - pe.post.totalTopLevelComments=comments.total; - pe.post.getAllReplyIDs(postIDs); - } - } - } - } - } - HashMap interactions=PostStorage.getPostInteractions(postIDs, self.user.id); + Map interactions=context(req).getWallController().getUserInteractions(feedPosts, self.user); if(!feed.isEmpty() && startFromID==0) startFromID=feed.get(0).id; jsLangKey(req, "yes", "no", "delete_post", "delete_post_confirm", "delete", "post_form_cw", "post_form_cw_placeholder", "cancel", "attach_menu_photo", "attach_menu_cw", "attach_menu_poll", "max_file_size_exceeded", "max_attachment_count_exceeded", "remove_attachment"); jsLangKey(req, "create_poll_question", "create_poll_options", "create_poll_add_option", "create_poll_delete_option", "create_poll_multi_choice", "create_poll_anonymous", "create_poll_time_limit", "X_days", "X_hours"); return new RenderedTemplateResponse("feed", req).with("title", Utils.lang(req).get("feed")).with("feed", feed).with("postInteractions", interactions) - .with("paginationURL", "/feed?startFrom="+startFromID+"&offset=").with("total", total[0]).with("offset", offset) + .with("paginationUrlPrefix", "/feed?startFrom="+startFromID+"&offset=").with("totalItems", total[0]).with("paginationOffset", offset).with("paginationPerPage", 25).with("paginationFirstPageUrl", "/feed") .with("draftAttachments", Utils.sessionInfo(req).postDraftAttachments); } @@ -531,15 +500,17 @@ public static Object likePopover(Request req, Response resp) throws SQLException return gson.toJson(o); } - public static Object likeList(Request req, Response resp) throws SQLException{ + public static Object likeList(Request req, Response resp){ + SessionInfo info=Utils.sessionInfo(req); + @Nullable Account self=info!=null ? info.account : null; int postID=Utils.parseIntOrDefault(req.params(":postID"), 0); - Post post=getPostOrThrow(postID); - int offset=parseIntOrDefault(req.queryParams("offset"), 0); - List users=UserStorage.getByIdAsList(LikeStorage.getPostLikes(postID, 0, offset, 100)); - RenderedTemplateResponse model=new RenderedTemplateResponse(isAjax(req) ? "user_grid" : "content_wrap", req).with("users", users); - UserInteractions interactions=PostStorage.getPostInteractions(Collections.singletonList(postID), 0).get(postID); - model.with("pageOffset", offset).with("total", interactions.likeCount).with("paginationUrlPrefix", "/posts/"+postID+"/likes?fromPagination&offset=").with("emptyMessage", lang(req).get("likes_empty")); - model.with("summary", lang(req).get("liked_by_X_people", Map.of("count", interactions.likeCount))); + Post post=context(req).getWallController().getPostOrThrow(postID); + int offset=offset(req); + PaginatedList likes=context(req).getUserInteractionsController().getLikesForObject(post, self!=null ? self.user : null, offset, 100); + RenderedTemplateResponse model=new RenderedTemplateResponse(isAjax(req) ? "user_grid" : "content_wrap", req) + .paginate(likes, "/posts/"+postID+"/likes?fromPagination&offset=", null) + .with("emptyMessage", lang(req).get("likes_empty")) + .with("summary", lang(req).get("liked_by_X_people", Map.of("count", likes.total))); if(isAjax(req)){ if(req.queryParams("fromPagination")==null) return new WebDeltaResponse(resp).box(lang(req).get("likes_title"), model.renderToString(), "likesList", 474); @@ -550,109 +521,69 @@ public static Object likeList(Request req, Response resp) throws SQLException{ return model; } - public static Object wallAll(Request req, Response resp) throws SQLException{ - return wall(req, resp, false); + public static Object userWallAll(Request req, Response resp){ + User user=context(req).getUsersController().getUserOrThrow(safeParseInt(req.params(":id"))); + return wall(req, resp, user, false); } - public static Object wallOwn(Request req, Response resp) throws SQLException{ - return wall(req, resp, true); + public static Object userWallOwn(Request req, Response resp){ + User user=context(req).getUsersController().getUserOrThrow(safeParseInt(req.params(":id"))); + return wall(req, resp, user, true); } - private static Object wall(Request req, Response resp, boolean ownOnly) throws SQLException{ - String username=req.params(":username"); - User user=UserStorage.getByUsername(username); - Group group=null; - if(user==null){ - group=GroupStorage.getByUsername(username); - if(group==null){ - throw new ObjectNotFoundException("err_user_not_found"); - }else if(ownOnly){ - resp.redirect(Config.localURI("/"+username+"/wall").toString()); - return ""; - } - } + public static Object groupWall(Request req, Response resp){ + Group group=context(req).getGroupsController().getGroupOrThrow(safeParseInt(req.params(":id"))); + return wall(req, resp, group, false); + } + + private static Object wall(Request req, Response resp, Actor owner, boolean ownOnly){ SessionInfo info=Utils.sessionInfo(req); @Nullable Account self=info!=null ? info.account : null; - - int[] postCount={0}; - int offset=Utils.parseIntOrDefault(req.queryParams("offset"), 0); - List wall=PostStorage.getWallPosts(user==null ? group.id : user.id, group!=null, 0, 0, offset, postCount, ownOnly); - Set postIDs=wall.stream().map((Post p)->p.id).collect(Collectors.toSet()); - + int offset=offset(req); + PaginatedList wall=context(req).getWallController().getWallPosts(owner, ownOnly, offset, 20); if(req.attribute("mobile")==null){ - Map> allComments=PostStorage.getRepliesForFeed(postIDs); - for(Post post:wall){ - ListAndTotal comments=allComments.get(post.id); - if(comments!=null){ - post.repliesObjects=comments.list; - post.totalTopLevelComments=comments.total; - post.getAllReplyIDs(postIDs); - } - } + context(req).getWallController().populateCommentPreviews(wall.list); } + Map interactions=context(req).getWallController().getUserInteractions(wall.list, self!=null ? self.user : null); - HashMap interactions=PostStorage.getPostInteractions(postIDs, self!=null ? self.user.id : 0); RenderedTemplateResponse model=new RenderedTemplateResponse("wall_page", req) - .with("posts", wall) + .paginate(wall) .with("postInteractions", interactions) - .with("owner", user!=null ? user : group) - .with("isGroup", group!=null) - .with("postCount", postCount[0]) - .with("pageOffset", offset) + .with("owner", owner) + .with("isGroup", owner instanceof Group) .with("ownOnly", ownOnly) - .with("paginationUrlPrefix", Config.localURI("/"+username+"/wall"+(ownOnly ? "/own" : ""))) .with("tab", ownOnly ? "own" : "all"); - if(user!=null){ - model.with("title", lang(req).get("wall_of_X", Map.of("name", user.getFirstAndGender()))); + if(owner instanceof User user){ + model.pageTitle(lang(req).get("wall_of_X", Map.of("name", user.getFirstAndGender()))); }else{ - model.with("title", lang(req).get("wall_of_group")); + model.pageTitle(lang(req).get("wall_of_group")); } return model; } - public static Object wallToWall(Request req, Response resp) throws SQLException{ - String username=req.params(":username"); - User user=UserStorage.getByUsername(username); - if(user==null) - throw new ObjectNotFoundException("err_user_not_found"); - String otherUsername=req.params(":other_username"); - User otherUser=UserStorage.getByUsername(otherUsername); - if(otherUser==null) - throw new ObjectNotFoundException("err_user_not_found"); + public static Object wallToWall(Request req, Response resp){ + User user=context(req).getUsersController().getUserOrThrow(safeParseInt(req.params(":id"))); + User otherUser=context(req).getUsersController().getUserOrThrow(safeParseInt(req.params(":otherUserID"))); SessionInfo info=Utils.sessionInfo(req); @Nullable Account self=info!=null ? info.account : null; - int[] postCount={0}; - int offset=Utils.parseIntOrDefault(req.queryParams("offset"), 0); - List wall=PostStorage.getWallToWall(user.id, otherUser.id, offset, postCount); - Set postIDs=wall.stream().map((Post p)->p.id).collect(Collectors.toSet()); - + int offset=offset(req); + PaginatedList wall=context(req).getWallController().getWallToWallPosts(user, otherUser, offset, 20); if(req.attribute("mobile")==null){ - Map> allComments=PostStorage.getRepliesForFeed(postIDs); - for(Post post:wall){ - ListAndTotal comments=allComments.get(post.id); - if(comments!=null){ - post.repliesObjects=comments.list; - post.totalTopLevelComments=comments.total; - post.getAllReplyIDs(postIDs); - } - } + context(req).getWallController().populateCommentPreviews(wall.list); } + Map interactions=context(req).getWallController().getUserInteractions(wall.list, self!=null ? self.user : null); - HashMap interactions=PostStorage.getPostInteractions(postIDs, self!=null ? self.user.id : 0); return new RenderedTemplateResponse("wall_page", req) - .with("posts", wall) + .paginate(wall) .with("postInteractions", interactions) .with("owner", user) .with("otherUser", otherUser) - .with("postCount", postCount[0]) - .with("pageOffset", offset) - .with("paginationUrlPrefix", Config.localURI("/"+username+"/wall/with/"+otherUsername)) .with("tab", "wall2wall") - .with("title", lang(req).get("wall_of_X", Map.of("name", user.getFirstAndGender()))); + .pageTitle(lang(req).get("wall_of_X", Map.of("name", user.getFirstAndGender()))); } public static Object ajaxCommentPreview(Request req, Response resp) throws SQLException{ @@ -774,39 +705,16 @@ public static Object pollOptionVotersPopover(Request req, Response resp) throws public static Object commentsFeed(Request req, Response resp, Account self) throws SQLException{ int offset=parseIntOrDefault(req.queryParams("offset"), 0); - ListAndTotal feed=PostStorage.getCommentsFeed(self.user.id, offset, 25); - HashSet postIDs=new HashSet<>(); - for(NewsfeedEntry e:feed.list){ - if(e instanceof PostNewsfeedEntry){ - PostNewsfeedEntry pe=(PostNewsfeedEntry) e; - if(pe.post!=null){ - postIDs.add(pe.post.id); - }else{ - System.err.println("No post: "+pe); - } - } - } - if(req.attribute("mobile")==null && !postIDs.isEmpty()){ - Map> allComments=PostStorage.getRepliesForFeed(postIDs); - for(NewsfeedEntry e:feed.list){ - if(e instanceof PostNewsfeedEntry){ - PostNewsfeedEntry pe=(PostNewsfeedEntry) e; - if(pe.post!=null){ - ListAndTotal comments=allComments.get(pe.post.id); - if(comments!=null){ - pe.post.repliesObjects=comments.list; - pe.post.totalTopLevelComments=comments.total; - pe.post.getAllReplyIDs(postIDs); - } - } - } - } + PaginatedList feed=PostStorage.getCommentsFeed(self.user.id, offset, 25); + List feedPosts=feed.list.stream().filter(e->e instanceof PostNewsfeedEntry pe && pe.post!=null).map(e->((PostNewsfeedEntry)e).post).collect(Collectors.toList()); + if(req.attribute("mobile")==null && !feedPosts.isEmpty()){ + context(req).getWallController().populateCommentPreviews(feedPosts); } - HashMap interactions=PostStorage.getPostInteractions(postIDs, self.user.id); + Map interactions=context(req).getWallController().getUserInteractions(feedPosts, self.user); jsLangKey(req, "yes", "no", "delete_post", "delete_post_confirm", "delete", "post_form_cw", "post_form_cw_placeholder", "cancel", "attach_menu_photo", "attach_menu_cw", "attach_menu_poll", "max_file_size_exceeded", "max_attachment_count_exceeded", "remove_attachment"); jsLangKey(req, "create_poll_question", "create_poll_options", "create_poll_add_option", "create_poll_delete_option", "create_poll_multi_choice", "create_poll_anonymous", "create_poll_time_limit", "X_days", "X_hours"); return new RenderedTemplateResponse("feed", req).with("title", Utils.lang(req).get("feed")).with("feed", feed.list).with("postInteractions", interactions) - .with("paginationURL", "/feed/comments?offset=").with("total", feed.total).with("offset", offset).with("paginationFirstURL", "/feed/comments").with("tab", "comments") + .with("paginationUrlPrefix", "/feed/comments?offset=").with("totalItems", feed.total).with("paginationOffset", offset).with("paginationFirstPageUrl", "/feed/comments").with("tab", "comments").with("paginationPerPage", 25) .with("draftAttachments", Utils.sessionInfo(req).postDraftAttachments); } } diff --git a/src/main/java/smithereen/routes/ProfileRoutes.java b/src/main/java/smithereen/routes/ProfileRoutes.java index 389b5fd0..0aba0861 100644 --- a/src/main/java/smithereen/routes/ProfileRoutes.java +++ b/src/main/java/smithereen/routes/ProfileRoutes.java @@ -26,7 +26,7 @@ import smithereen.data.FriendRequest; import smithereen.data.FriendshipStatus; import smithereen.data.Group; -import smithereen.data.ListAndTotal; +import smithereen.data.PaginatedList; import smithereen.data.Post; import smithereen.data.SessionInfo; import smithereen.data.SizedImage; @@ -56,27 +56,20 @@ public static Object profile(Request req, Response resp) throws SQLException{ Lang l=lang(req); if(user!=null){ boolean isSelf=self!=null && self.user.id==user.id; - int[] postCount={0}; - int offset=Utils.parseIntOrDefault(req.queryParams("offset"), 0); - List wall=PostStorage.getWallPosts(user.id, false, 0, 0, offset, postCount, false); - RenderedTemplateResponse model=new RenderedTemplateResponse("profile", req).with("title", user.getFullName()).with("user", user).with("wall", wall).with("own", self!=null && self.user.id==user.id).with("postCount", postCount[0]); - model.with("pageOffset", offset); - - Set postIDs=wall.stream().map((Post p)->p.id).collect(Collectors.toSet()); + int offset=offset(req); + PaginatedList wall=context(req).getWallController().getWallPosts(user, false, offset, 20); + RenderedTemplateResponse model=new RenderedTemplateResponse("profile", req) + .pageTitle(user.getFullName()) + .with("user", user) + .with("own", self!=null && self.user.id==user.id) + .with("postCount", wall.total) + .paginate(wall); if(req.attribute("mobile")==null){ - Map> allComments=PostStorage.getRepliesForFeed(postIDs); - for(Post post:wall){ - ListAndTotal comments=allComments.get(post.id); - if(comments!=null){ - post.repliesObjects=comments.list; - post.totalTopLevelComments=comments.total; - post.getAllReplyIDs(postIDs); - } - } + context(req).getWallController().populateCommentPreviews(wall.list); } - HashMap interactions=PostStorage.getPostInteractions(postIDs, self!=null ? self.user.id : 0); + Map interactions=context(req).getWallController().getUserInteractions(wall.list, self!=null ? self.user : null); model.with("postInteractions", interactions); int[] friendCount={0}; @@ -145,7 +138,7 @@ public static Object profile(Request req, Response resp) throws SQLException{ meta.put("og:first_name", user.firstName); if(StringUtils.isNotEmpty(user.lastName)) meta.put("og:last_name", user.lastName); - String descr=l.get("X_friends", Map.of("count", friendCount[0]))+", "+l.get("X_posts", Map.of("count", postCount[0])); + String descr=l.get("X_friends", Map.of("count", friendCount[0]))+", "+l.get("X_posts", Map.of("count", wall.total)); if(StringUtils.isNotEmpty(user.summary)) descr+="\n"+Jsoup.clean(user.summary, Whitelist.none()); meta.put("og:description", descr); @@ -172,7 +165,7 @@ else if(user.gender==User.Gender.FEMALE) model.addNavBarItem(user.getFullName(), null, isSelf ? l.get("this_is_you") : null); - model.with("groups", GroupStorage.getUserGroups(user.id)); + model.with("groups", context(req).getGroupsController().getUserGroups(user, 0, 100).list); jsLangKey(req, "yes", "no", "delete_post", "delete_post_confirm", "delete_reply", "delete_reply_confirm", "remove_friend", "cancel", "delete", "post_form_cw", "post_form_cw_placeholder", "attach_menu_photo", "attach_menu_cw", "attach_menu_poll", "max_file_size_exceeded", "max_attachment_count_exceeded", "remove_attachment"); jsLangKey(req, "create_poll_question", "create_poll_options", "create_poll_add_option", "create_poll_delete_option", "create_poll_multi_choice", "create_poll_anonymous", "create_poll_time_limit", "X_days", "X_hours"); return model; @@ -196,33 +189,34 @@ public static Object confirmSendFriendRequest(Request req, Response resp, Accoun ensureUserNotBlocked(self.user, user); FriendshipStatus status=UserStorage.getFriendshipStatus(self.user.id, user.id); Lang l=lang(req); - if(status==FriendshipStatus.FOLLOWED_BY){ - if(isAjax(req) && verifyCSRF(req, resp)){ - UserStorage.followUser(self.user.id, user.id, !(user instanceof ForeignUser)); - if(user instanceof ForeignUser){ - ActivityPubWorker.getInstance().sendFollowActivity(self.user, (ForeignUser)user); + switch(status){ + case FOLLOWED_BY: + if(isAjax(req) && verifyCSRF(req, resp)){ + UserStorage.followUser(self.user.id, user.id, !(user instanceof ForeignUser)); + if(user instanceof ForeignUser){ + ActivityPubWorker.getInstance().sendFollowActivity(self.user, (ForeignUser) user); + } + return new WebDeltaResponse(resp).refresh(); + }else{ + RenderedTemplateResponse model=new RenderedTemplateResponse("form_page", req); + model.with("targetUser", user); + model.with("contentTemplate", "send_friend_request").with("formAction", user.getProfileURL("doSendFriendRequest")).with("submitButton", l.get("add_friend")); + return model; } - return new WebDeltaResponse(resp).refresh(); - }else{ - RenderedTemplateResponse model=new RenderedTemplateResponse("form_page", req); - model.with("targetUser", user); - model.with("contentTemplate", "send_friend_request").with("formAction", user.getProfileURL("doSendFriendRequest")).with("submitButton", l.get("add_friend")); - return model; - } - }else if(status==FriendshipStatus.NONE){ - if(user.supportsFriendRequests()){ - RenderedTemplateResponse model=new RenderedTemplateResponse("send_friend_request", req); - model.with("targetUser", user); - return wrapForm(req, resp, "send_friend_request", user.getProfileURL("doSendFriendRequest"), l.get("send_friend_req_title"), "add_friend", model); - }else{ - return doSendFriendRequest(req, resp, self); - } - }else if(status==FriendshipStatus.FRIENDS){ - return wrapError(req, resp, "err_already_friends"); - }else if(status==FriendshipStatus.REQUEST_RECVD){ - return wrapError(req, resp, "err_have_incoming_friend_req"); - }else{ // REQ_SENT - return wrapError(req, resp, "err_friend_req_already_sent"); + case NONE: + if(user.supportsFriendRequests()){ + RenderedTemplateResponse model=new RenderedTemplateResponse("send_friend_request", req); + model.with("targetUser", user); + return wrapForm(req, resp, "send_friend_request", user.getProfileURL("doSendFriendRequest"), l.get("send_friend_req_title"), "add_friend", model); + }else{ + return doSendFriendRequest(req, resp, self); + } + case FRIENDS: + return wrapError(req, resp, "err_already_friends"); + case REQUEST_RECVD: + return wrapError(req, resp, "err_have_incoming_friend_req"); + default: // REQ_SENT + return wrapError(req, resp, "err_friend_req_already_sent"); } }else{ throw new ObjectNotFoundException("err_user_not_found"); @@ -289,8 +283,9 @@ public static Object confirmRemoveFriend(Request req, Response resp, Account sel private static Object friends(Request req, Response resp, User user, Account self) throws SQLException{ RenderedTemplateResponse model=new RenderedTemplateResponse("friends", req); - model.with("friendList", UserStorage.getFriendListForUser(user.id)).with("owner", user).with("tab", 0); - model.with("title", lang(req).get("friends")); + model.with("owner", user); + model.pageTitle(lang(req).get("friends")); + model.paginate(context(req).getFriendsController().getFriends(user, offset(req), 100)); if(self!=null && user.id!=self.user.id){ int mutualCount=UserStorage.getMutualFriendsCount(self.user.id, user.id); model.with("mutualCount", mutualCount); @@ -316,8 +311,9 @@ public static Object mutualFriends(Request req, Response resp, Account self) thr if(user.id==self.user.id) throw new ObjectNotFoundException("err_user_not_found"); RenderedTemplateResponse model=new RenderedTemplateResponse("friends", req); - model.with("friendList", UserStorage.getMutualFriendListForUser(user.id, self.user.id)).with("owner", user).with("tab", 0); - model.with("title", lang(req).get("friends")); + model.paginate(context(req).getFriendsController().getMutualFriends(user, self.user, offset(req), 100)); + model.with("owner", user); + model.pageTitle(lang(req).get("friends")); model.with("tab", "mutual"); int mutualCount=UserStorage.getMutualFriendsCount(self.user.id, user.id); model.with("mutualCount", mutualCount); @@ -328,62 +324,57 @@ public static Object mutualFriends(Request req, Response resp, Account self) thr public static Object followers(Request req, Response resp) throws SQLException{ SessionInfo info=Utils.sessionInfo(req); @Nullable Account self=info!=null ? info.account : null; - String username=req.params(":username"); + String _id=req.params(":id"); User user; - if(username==null){ + if(_id==null){ if(requireAccount(req, resp)){ - user=sessionInfo(req).account.user; + user=self.user; }else{ return ""; } }else{ - user=UserStorage.getByUsername(username); + user=context(req).getUsersController().getUserOrThrow(safeParseInt(_id)); } - if(user!=null){ - RenderedTemplateResponse model=new RenderedTemplateResponse("friends", req); - model.with("title", lang(req).get("followers")).with("toolbarTitle", lang(req).get("friends")); - model.with("friendList", UserStorage.getNonMutualFollowers(user.id, true, true)).with("owner", user).with("followers", true).with("tab", "followers"); - if(self!=null && user.id!=self.user.id){ - int mutualCount=UserStorage.getMutualFriendsCount(self.user.id, user.id); - model.with("mutualCount", mutualCount); - } - return model; + RenderedTemplateResponse model=new RenderedTemplateResponse("friends", req); + model.with("title", lang(req).get("followers")).with("toolbarTitle", lang(req).get("friends")); + model.paginate(context(req).getFriendsController().getFollowers(user, offset(req), 100)); + model.with("owner", user).with("followers", true).with("tab", "followers"); + if(self!=null && user.id!=self.user.id){ + int mutualCount=UserStorage.getMutualFriendsCount(self.user.id, user.id); + model.with("mutualCount", mutualCount); } - throw new ObjectNotFoundException("err_user_not_found"); + return model; } public static Object following(Request req, Response resp) throws SQLException{ SessionInfo info=Utils.sessionInfo(req); @Nullable Account self=info!=null ? info.account : null; - String username=req.params(":username"); + String _id=req.params(":id"); User user; - if(username==null){ + if(_id==null){ if(requireAccount(req, resp)){ - user=sessionInfo(req).account.user; + user=self.user; }else{ return ""; } }else{ - user=UserStorage.getByUsername(username); + user=context(req).getUsersController().getUserOrThrow(safeParseInt(_id)); } - if(user!=null){ - RenderedTemplateResponse model=new RenderedTemplateResponse("friends", req); - model.with("title", lang(req).get("following")).with("toolbarTitle", lang(req).get("friends")); - model.with("friendList", UserStorage.getNonMutualFollowers(user.id, false, true)).with("owner", user).with("following", true).with("tab", "following"); - if(self!=null && user.id!=self.user.id){ - int mutualCount=UserStorage.getMutualFriendsCount(self.user.id, user.id); - model.with("mutualCount", mutualCount); - } - jsLangKey(req, "unfollow", "yes", "no"); - return model; + RenderedTemplateResponse model=new RenderedTemplateResponse("friends", req); + model.with("title", lang(req).get("following")).with("toolbarTitle", lang(req).get("friends")); + model.paginate(context(req).getFriendsController().getFollows(user, offset(req), 100)); + model.with("owner", user).with("following", true).with("tab", "following"); + if(self!=null && user.id!=self.user.id){ + int mutualCount=UserStorage.getMutualFriendsCount(self.user.id, user.id); + model.with("mutualCount", mutualCount); } - throw new ObjectNotFoundException("err_user_not_found"); + jsLangKey(req, "unfollow", "yes", "no"); + return model; } public static Object incomingFriendRequests(Request req, Response resp, Account self) throws SQLException{ - List requests=UserStorage.getIncomingFriendRequestsForUser(self.user.id, 0, 100); RenderedTemplateResponse model=new RenderedTemplateResponse("friend_requests", req); - model.with("friendRequests", requests); + model.paginate(context(req).getFriendsController().getIncomingFriendRequests(self.user, offset(req), 20)); model.with("title", lang(req).get("friend_requests")).with("toolbarTitle", lang(req).get("friends")).with("owner", self.user); return model; } diff --git a/src/main/java/smithereen/sparkext/ActivityPubCollectionPageResponse.java b/src/main/java/smithereen/sparkext/ActivityPubCollectionPageResponse.java index 75f10c7e..bc8121c6 100644 --- a/src/main/java/smithereen/sparkext/ActivityPubCollectionPageResponse.java +++ b/src/main/java/smithereen/sparkext/ActivityPubCollectionPageResponse.java @@ -6,7 +6,7 @@ import smithereen.activitypub.objects.ActivityPubObject; import smithereen.activitypub.objects.LinkOrObject; -import smithereen.data.ListAndTotal; +import smithereen.data.PaginatedList; public class ActivityPubCollectionPageResponse{ public int totalItems; @@ -27,11 +27,11 @@ public static ActivityPubCollectionPageResponse forLinks(List objects, int return r; } - public static ActivityPubCollectionPageResponse forLinks(ListAndTotal lt){ + public static ActivityPubCollectionPageResponse forLinks(PaginatedList lt){ return forLinks(lt.list, lt.total); } - public static ActivityPubCollectionPageResponse forObjects(ListAndTotal lt){ + public static ActivityPubCollectionPageResponse forObjects(PaginatedList lt){ return forObjects(lt.list, lt.total); } diff --git a/src/main/java/smithereen/storage/DatabaseSchemaUpdater.java b/src/main/java/smithereen/storage/DatabaseSchemaUpdater.java index c8c56b03..4f51a7dd 100644 --- a/src/main/java/smithereen/storage/DatabaseSchemaUpdater.java +++ b/src/main/java/smithereen/storage/DatabaseSchemaUpdater.java @@ -12,7 +12,7 @@ import smithereen.Utils; public class DatabaseSchemaUpdater{ - public static final int SCHEMA_VERSION=16; + public static final int SCHEMA_VERSION=17; private static final Logger LOG=LoggerFactory.getLogger(DatabaseSchemaUpdater.class); public static void maybeUpdate() throws SQLException{ @@ -281,6 +281,10 @@ PRIMARY KEY (`object_type`,`object_id`,`user_id`), conn.createStatement().execute("ALTER TABLE `wall_posts` ADD `federation_state` tinyint unsigned NOT NULL DEFAULT 0"); }else if(target==16){ conn.createStatement().execute("ALTER TABLE `wall_posts` ADD `source` text DEFAULT NULL, ADD `source_format` tinyint unsigned DEFAULT NULL"); + }else if(target==17){ + conn.createStatement().execute("ALTER TABLE `users` ADD `about_source` TEXT NULL AFTER `about`"); + conn.createStatement().execute("ALTER TABLE `groups` ADD `about_source` TEXT NULL AFTER `about`"); + conn.createStatement().execute("ALTER TABLE `friend_requests` ADD `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST"); } } } diff --git a/src/main/java/smithereen/storage/DatabaseUtils.java b/src/main/java/smithereen/storage/DatabaseUtils.java index c742b3c2..b947a83a 100644 --- a/src/main/java/smithereen/storage/DatabaseUtils.java +++ b/src/main/java/smithereen/storage/DatabaseUtils.java @@ -1,9 +1,13 @@ package smithereen.storage; import java.sql.Connection; +import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Spliterator; @@ -138,6 +142,16 @@ public void forEachRemaining(Consumer action){ } } + public static Instant getInstant(ResultSet res, String name) throws SQLException{ + Timestamp ts=res.getTimestamp(name); + return ts==null ? null : ts.toInstant(); + } + + public static LocalDate getLocalDate(ResultSet res, String name) throws SQLException{ + Date date=res.getDate(name); + return date==null ? null : date.toLocalDate(); + } + private static class UncheckedSQLException extends RuntimeException{ public UncheckedSQLException(SQLException cause){ super(cause); diff --git a/src/main/java/smithereen/storage/GroupStorage.java b/src/main/java/smithereen/storage/GroupStorage.java index 71594fdb..a77a002d 100644 --- a/src/main/java/smithereen/storage/GroupStorage.java +++ b/src/main/java/smithereen/storage/GroupStorage.java @@ -31,7 +31,7 @@ import smithereen.data.ForeignGroup; import smithereen.data.Group; import smithereen.data.GroupAdmin; -import smithereen.data.ListAndTotal; +import smithereen.data.PaginatedList; import smithereen.data.User; import spark.utils.StringUtils; @@ -43,7 +43,7 @@ public class GroupStorage{ private static final Object adminUpdateLock=new Object(); - public static int createGroup(String name, String username, int userID) throws SQLException{ + public static int createGroup(String name, String description, String descriptionSrc, int userID, boolean isEvent) throws SQLException{ int id; Connection conn=DatabaseConnectionManager.getConnection(); try{ @@ -53,15 +53,31 @@ public static int createGroup(String name, String username, int userID) throws S kpg.initialize(2048); KeyPair pair=kpg.generateKeyPair(); - PreparedStatement stmt=new SQLQueryBuilder(conn) - .insertInto("groups") - .value("name", name) - .value("username", username) - .value("public_key", pair.getPublic().getEncoded()) - .value("private_key", pair.getPrivate().getEncoded()) - .value("member_count", 1) - .createStatement(Statement.RETURN_GENERATED_KEYS); - id=DatabaseUtils.insertAndGetID(stmt); + PreparedStatement stmt; + String username; + synchronized(GroupStorage.class){ + SQLQueryBuilder bldr=new SQLQueryBuilder(conn) + .insertInto("groups") + .value("name", name) + .value("username", "__tmp"+System.currentTimeMillis()) + .value("public_key", pair.getPublic().getEncoded()) + .value("private_key", pair.getPrivate().getEncoded()) + .value("member_count", 1) + .value("about", description) + .value("about_source", descriptionSrc) + .value("type", isEvent ? Group.Type.EVENT : Group.Type.GROUP); + + stmt=bldr.createStatement(Statement.RETURN_GENERATED_KEYS); + id=DatabaseUtils.insertAndGetID(stmt); + username=(isEvent ? "event" : "club")+id; + + new SQLQueryBuilder(conn) + .update("groups") + .value("username", username) + .where("id=?", id) + .createStatement() + .execute(); + } new SQLQueryBuilder(conn) .insertInto("group_memberships") @@ -320,7 +336,7 @@ public static List getRandomMembersForProfile(int groupID) throws SQLExcep } } - public static ListAndTotal getMembers(int groupID, int offset, int count) throws SQLException{ + public static PaginatedList getMembers(int groupID, int offset, int count) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); PreparedStatement stmt=new SQLQueryBuilder(conn) .selectFrom("group_memberships") @@ -328,13 +344,15 @@ public static ListAndTotal getMembers(int groupID, int offset, int count) .where("group_id=? AND accepted=1", groupID) .createStatement(); int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery()); + if(total==0) + return PaginatedList.emptyList(count); stmt=new SQLQueryBuilder(conn) .selectFrom("group_memberships") .where("group_id=? AND accepted=1", groupID) .limit(count, offset) .createStatement(); try(ResultSet res=stmt.executeQuery()){ - return new ListAndTotal<>(UserStorage.getByIdAsList(DatabaseUtils.intResultSetToList(res)), total); + return new PaginatedList<>(UserStorage.getByIdAsList(DatabaseUtils.intResultSetToList(res)), total, offset, count); } } @@ -407,20 +425,35 @@ public static void leaveGroup(Group group, int userID, boolean tentative) throws } } - public static List getUserGroups(int userID) throws SQLException{ - PreparedStatement stmt=new SQLQueryBuilder().selectFrom("group_memberships").columns("group_id").where("user_id=? AND accepted=1", userID).createStatement(); + public static PaginatedList getUserGroups(int userID, int offset, int count) throws SQLException{ + Connection conn=DatabaseConnectionManager.getConnection(); + PreparedStatement stmt=new SQLQueryBuilder(conn) + .selectFrom("group_memberships") + .count() + .where("user_id=? AND accepted=1", userID) + .createStatement(); + int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery()); + if(total==0) + return new PaginatedList<>(Collections.emptyList(), 0, 0, count); + stmt=new SQLQueryBuilder() + .selectFrom("group_memberships") + .columns("group_id") + .where("user_id=? AND accepted=1", userID) + .orderBy("group_id ASC") + .limit(count, offset) + .createStatement(); try(ResultSet res=stmt.executeQuery()){ - return getByIdAsList(DatabaseUtils.intResultSetToList(res)); + return new PaginatedList<>(getByIdAsList(DatabaseUtils.intResultSetToList(res)), total, offset, count); } } - public static ListAndTotal getUserGroupIDs(int userID, int offset, int count) throws SQLException{ + public static PaginatedList getUserGroupIDs(int userID, int offset, int count) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); PreparedStatement stmt=conn.prepareStatement("SELECT count(*) FROM group_memberships WHERE user_id=? AND accepted=1"); stmt.setInt(1, userID); int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery()); if(total==0) - return new ListAndTotal<>(Collections.emptyList(), 0); + return new PaginatedList<>(Collections.emptyList(), 0); stmt=conn.prepareStatement("SELECT group_id, ap_id FROM group_memberships JOIN groups ON group_id=id WHERE user_id=? AND accepted=1 LIMIT ? OFFSET ?"); stmt.setInt(1, userID); @@ -433,14 +466,23 @@ public static ListAndTotal getUserGroupIDs(int userID, int offset, int coun String apID=res.getString(2); list.add(apID!=null ? URI.create(apID) : Config.localURI("/groups/"+res.getInt(1))); } - return new ListAndTotal<>(list, total); + return new PaginatedList<>(list, total); } } - public static List getUserManagedGroups(int userID) throws SQLException{ - PreparedStatement stmt=new SQLQueryBuilder().selectFrom("group_admins").columns("group_id").where("user_id=?", userID).createStatement(); + public static PaginatedList getUserManagedGroups(int userID, int offset, int count) throws SQLException{ + Connection conn=DatabaseConnectionManager.getConnection(); + PreparedStatement stmt=new SQLQueryBuilder(conn) + .selectFrom("group_admins") + .count() + .where("user_id=?", userID) + .createStatement(); + int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery()); + if(total==0) + return PaginatedList.emptyList(count); + stmt=new SQLQueryBuilder(conn).selectFrom("group_admins").columns("group_id").where("user_id=?", userID).createStatement(); try(ResultSet res=stmt.executeQuery()){ - return getByIdAsList(DatabaseUtils.intResultSetToList(res)); + return new PaginatedList<>(getByIdAsList(DatabaseUtils.intResultSetToList(res)), total, offset, count); } } diff --git a/src/main/java/smithereen/storage/LikeStorage.java b/src/main/java/smithereen/storage/LikeStorage.java index 6c7e98c9..384774dc 100644 --- a/src/main/java/smithereen/storage/LikeStorage.java +++ b/src/main/java/smithereen/storage/LikeStorage.java @@ -14,7 +14,7 @@ import smithereen.activitypub.objects.LinkOrObject; import smithereen.activitypub.objects.activities.Like; import smithereen.data.ForeignUser; -import smithereen.data.ListAndTotal; +import smithereen.data.PaginatedList; import smithereen.data.User; public class LikeStorage{ @@ -59,7 +59,7 @@ private static int deleteLike(int userID, int objectID, Like.ObjectType type) th } } - public static ListAndTotal getLikes(int objectID, URI objectApID, Like.ObjectType type, int offset, int count) throws SQLException{ + public static PaginatedList getLikes(int objectID, URI objectApID, Like.ObjectType type, int offset, int count) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT likes.id AS like_id, user_id, likes.ap_id AS like_ap_id, users.ap_id AS user_ap_id FROM likes JOIN users ON users.id=likes.user_id WHERE object_id=? AND object_type=? ORDER BY likes.id ASC LIMIT ?,?", objectID, type, offset, count); @@ -86,7 +86,7 @@ public static ListAndTotal getLikes(int objectID, URI objectApID, Like.Obj .count() .where("object_id=? AND object_type=?", objectID, type.ordinal()) .createStatement(); - return new ListAndTotal<>(likes, DatabaseUtils.oneFieldToInt(stmt.executeQuery())); + return new PaginatedList<>(likes, DatabaseUtils.oneFieldToInt(stmt.executeQuery())); } public static List getPostLikes(int objectID, int selfID, int offset, int count) throws SQLException{ diff --git a/src/main/java/smithereen/storage/NotificationsStorage.java b/src/main/java/smithereen/storage/NotificationsStorage.java index f92996a7..9c76be14 100644 --- a/src/main/java/smithereen/storage/NotificationsStorage.java +++ b/src/main/java/smithereen/storage/NotificationsStorage.java @@ -45,7 +45,7 @@ public static void putNotification(int owner, @NotNull Notification n) throws SQ un.incNewNotificationsCount(1); } - public static List getNotifications(int owner, int offset, @Nullable int[] total) throws SQLException{ + public static List getNotifications(int owner, int offset, int count, @Nullable int[] total) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); PreparedStatement stmt; if(total!=null){ @@ -56,9 +56,10 @@ public static List getNotifications(int owner, int offset, @Nullab total[0]=res.getInt(1); } } - stmt=conn.prepareStatement("SELECT * FROM `notifications` WHERE `owner_id`=? ORDER BY `time` DESC LIMIT ?,50"); + stmt=conn.prepareStatement("SELECT * FROM `notifications` WHERE `owner_id`=? ORDER BY `time` DESC LIMIT ?,?"); stmt.setInt(1, owner); stmt.setInt(2, offset); + stmt.setInt(3, count); try(ResultSet res=stmt.executeQuery()){ if(res.first()){ ArrayList notifications=new ArrayList<>(); @@ -68,7 +69,7 @@ public static List getNotifications(int owner, int offset, @Nullab return notifications; } } - return Collections.EMPTY_LIST; + return Collections.emptyList(); } public static void deleteNotificationsForObject(@NotNull Notification.ObjectType type, int objID) throws SQLException{ diff --git a/src/main/java/smithereen/storage/PostStorage.java b/src/main/java/smithereen/storage/PostStorage.java index 0c6ed78d..04d6d5e3 100644 --- a/src/main/java/smithereen/storage/PostStorage.java +++ b/src/main/java/smithereen/storage/PostStorage.java @@ -15,12 +15,11 @@ import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; -import java.sql.Types; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -32,7 +31,7 @@ import smithereen.Config; import smithereen.activitypub.objects.activities.Like; import smithereen.data.FederationState; -import smithereen.data.ListAndTotal; +import smithereen.data.PaginatedList; import smithereen.data.Poll; import smithereen.data.PollOption; import smithereen.data.UriBuilder; @@ -125,7 +124,7 @@ private static int putForeignPoll(Connection conn, int ownerID, URI activityPubI .value("owner_id", ownerID) .value("question", poll.question) .value("is_anonymous", poll.anonymous) - .value("end_time", poll.endTime!=null ? new Timestamp(poll.endTime.getTime()) : null) + .value("end_time", poll.endTime) .value("is_multi_choice", poll.multipleChoice) .value("num_voted_users",poll.numVoters) .createStatement(Statement.RETURN_GENERATED_KEYS); @@ -176,7 +175,7 @@ public static void putForeignWallPost(Post post) throws SQLException{ .value("ap_url", post.url.toString()) .value("ap_id", post.activityPubID.toString()) .value("reply_key", Utils.serializeIntArray(post.replyKey)) - .value("created_at", new Timestamp(post.published.getTime())) + .value("created_at", post.published) .value("mentions", post.mentionedUsers.isEmpty() ? null : Utils.serializeIntArray(post.mentionedUsers.stream().mapToInt(u->u.id).toArray())) .value("ap_replies", Objects.toString(post.getRepliesURL(), null)) .value("poll_id", post.poll!=null ? post.poll.id : null) @@ -255,7 +254,7 @@ public static void putForeignWallPost(Post post) throws SQLException{ .value("type", NewsfeedEntry.Type.POST) .value("author_id", post.user.id) .value("object_id", post.id) - .value("time", new Timestamp(post.published.getTime())) + .value("time", post.published) .createStatement() .execute(); } @@ -367,7 +366,7 @@ public static List getFeed(int userID, int startFromID, int offse return posts; } - public static List getWallPosts(int ownerID, boolean isGroup, int minID, int maxID, int offset, int[] total, boolean ownOnly) throws SQLException{ + public static List getWallPosts(int ownerID, boolean isGroup, int minID, int maxID, int offset, int count, int[] total, boolean ownOnly) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); PreparedStatement stmt; String ownCondition=ownOnly ? " AND owner_user_id=author_id" : ""; @@ -381,13 +380,13 @@ public static List getWallPosts(int ownerID, boolean isGroup, int minID, i } } if(minID>0){ - stmt=conn.prepareStatement("SELECT * FROM `wall_posts` WHERE `"+ownerField+"`=? AND `id`>? AND `reply_key` IS NULL"+ownCondition+" ORDER BY created_at DESC LIMIT 25"); + stmt=conn.prepareStatement("SELECT * FROM `wall_posts` WHERE `"+ownerField+"`=? AND `id`>? AND `reply_key` IS NULL"+ownCondition+" ORDER BY created_at DESC LIMIT "+count); stmt.setInt(2, minID); }else if(maxID>0){ - stmt=conn.prepareStatement("SELECT * FROM `wall_posts` WHERE `"+ownerField+"`=? AND `id`= posts=new ArrayList<>(); @@ -435,7 +434,7 @@ public static List getWallPostActivityPubIDs(int ownerID, boolean isGroup, } } - public static List getWallToWall(int userID, int otherUserID, int offset, int[] total) throws SQLException{ + public static List getWallToWall(int userID, int otherUserID, int offset, int count, int[] total) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); PreparedStatement stmt; if(total!=null){ @@ -449,7 +448,7 @@ public static List getWallToWall(int userID, int otherUserID, int offset, total[0]=res.getInt(1); } } - stmt=conn.prepareStatement("SELECT * FROM wall_posts WHERE ((owner_user_id=? AND author_id=?) OR (owner_user_id=? AND author_id=?)) AND `reply_key` IS NULL ORDER BY created_at DESC LIMIT "+offset+",25"); + stmt=conn.prepareStatement("SELECT * FROM wall_posts WHERE ((owner_user_id=? AND author_id=?) OR (owner_user_id=? AND author_id=?)) AND `reply_key` IS NULL ORDER BY created_at DESC LIMIT "+offset+","+count); stmt.setInt(1, userID); stmt.setInt(2, otherUserID); stmt.setInt(3, otherUserID); @@ -553,7 +552,7 @@ public static void deletePost(int id) throws SQLException{ } } - public static Map> getRepliesForFeed(Set postIDs) throws SQLException{ + public static Map> getRepliesForFeed(Set postIDs) throws SQLException{ if(postIDs.isEmpty()) return Collections.emptyMap(); Connection conn=DatabaseConnectionManager.getConnection(); @@ -564,12 +563,12 @@ public static Map> getRepliesForFeed(Set po i++; } LOG.debug("{}", stmt); - HashMap> map=new HashMap<>(); + HashMap> map=new HashMap<>(); try(ResultSet res=stmt.executeQuery()){ res.afterLast(); while(res.previous()){ Post post=Post.fromResultSet(res); - List posts=map.computeIfAbsent(post.getReplyChainElement(0), (k)->new ListAndTotal<>(new ArrayList<>(), 0)).list; + List posts=map.computeIfAbsent(post.getReplyChainElement(0), (k)->new PaginatedList<>(new ArrayList<>(), 0)).list; posts.add(post); } } @@ -967,7 +966,7 @@ public static synchronized int voteInPoll(int userID, int pollID, int optionID, return rVoteID; } - public static int createPoll(int ownerID, @NotNull String question, @NotNull List options, boolean anonymous, boolean multiChoice, @Nullable Date endTime) throws SQLException{ + public static int createPoll(int ownerID, @NotNull String question, @NotNull List options, boolean anonymous, boolean multiChoice, @Nullable Instant endTime) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); PreparedStatement stmt=new SQLQueryBuilder(conn) .insertInto("polls") @@ -975,7 +974,7 @@ public static int createPoll(int ownerID, @NotNull String question, @NotNull Lis .value("question", question) .value("is_anonymous", anonymous) .value("is_multi_choice", multiChoice) - .value("end_time", endTime!=null ? new Timestamp(endTime.getTime()) : null) + .value("end_time", endTime) .createStatement(Statement.RETURN_GENERATED_KEYS); stmt.execute(); int pollID; @@ -1039,7 +1038,7 @@ public static void setPostFederationState(int postID, FederationState state) thr .execute(); } - public static ListAndTotal getCommentsFeed(int userID, int offset, int count) throws SQLException{ + public static PaginatedList getCommentsFeed(int userID, int offset, int count) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); int total; PreparedStatement stmt=new SQLQueryBuilder(conn) @@ -1054,7 +1053,7 @@ public static ListAndTotal getCommentsFeed(int userID, int offset } if(total==0) - return new ListAndTotal<>(Collections.emptyList(), 0); + return new PaginatedList<>(Collections.emptyList(), 0); stmt=new SQLQueryBuilder(conn) .selectFrom("newsfeed_comments") @@ -1082,7 +1081,7 @@ public static ListAndTotal getCommentsFeed(int userID, int offset } } if(needPosts.isEmpty()) - return new ListAndTotal<>(entries, total); + return new PaginatedList<>(entries, total); stmt=new SQLQueryBuilder(conn) .selectFrom("wall_posts") @@ -1100,7 +1099,7 @@ public static ListAndTotal getCommentsFeed(int userID, int offset } } - return new ListAndTotal<>(entries, total); + return new PaginatedList<>(entries, total); } private static class DeleteCommentBookmarksRunnable implements Runnable{ diff --git a/src/main/java/smithereen/storage/SQLQueryBuilder.java b/src/main/java/smithereen/storage/SQLQueryBuilder.java index a1dfebef..0bf1467e 100644 --- a/src/main/java/smithereen/storage/SQLQueryBuilder.java +++ b/src/main/java/smithereen/storage/SQLQueryBuilder.java @@ -6,6 +6,9 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -357,8 +360,12 @@ private static class Value{ public Value(String key, Object value){ this.key=key; - if(value instanceof Enum) - this.value=((Enum)value).ordinal(); + if(value instanceof Enum e) + this.value=e.ordinal(); + else if(value instanceof Instant instant) + this.value=Timestamp.from(instant); + else if(value instanceof LocalDate localDate) + this.value=java.sql.Date.valueOf(localDate); else this.value=value; } diff --git a/src/main/java/smithereen/storage/UserStorage.java b/src/main/java/smithereen/storage/UserStorage.java index 5bd82f36..42f06097 100644 --- a/src/main/java/smithereen/storage/UserStorage.java +++ b/src/main/java/smithereen/storage/UserStorage.java @@ -38,6 +38,7 @@ import smithereen.data.FriendRequest; import smithereen.data.FriendshipStatus; import smithereen.data.Invitation; +import smithereen.data.PaginatedList; import smithereen.data.UriBuilder; import smithereen.data.User; import smithereen.data.UserNotifications; @@ -238,8 +239,26 @@ public static void putFriendRequest(int selfUserID, int targetUserID, String mes } } - public static List getFriendListForUser(int userID) throws SQLException{ - return getByIdAsList(getFriendIDsForUser(userID)); + public static PaginatedList getFriendListForUser(int userID, int offset, int count) throws SQLException{ + Connection conn=DatabaseConnectionManager.getConnection(); + PreparedStatement stmt=new SQLQueryBuilder(conn) + .selectFrom("followings") + .count() + .where("followee_id=? AND mutual=1", userID) + .createStatement(); + int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery()); + if(total==0) + return PaginatedList.emptyList(count); + stmt=new SQLQueryBuilder(conn) + .selectFrom("followings") + .columns("followee_id") + .where("follower_id=? AND mutual=1", userID) + .orderBy("followee_id ASC") + .limit(count, offset) + .createStatement(); + try(ResultSet res=stmt.executeQuery()){ + return new PaginatedList<>(getByIdAsList(DatabaseUtils.intResultSetToList(res)), total, offset, count); + } } public static List getFriendIDsForUser(int userID) throws SQLException{ @@ -320,28 +339,51 @@ public static List getMutualFriendIDsForUser(int userID, int otherUserI return DatabaseUtils.intResultSetToList(stmt.executeQuery()); } - public static List getMutualFriendListForUser(int userID, int otherUserID) throws SQLException{ - return getByIdAsList(getMutualFriendIDsForUser(userID, otherUserID, 0, 100)); + public static PaginatedList getMutualFriendListForUser(int userID, int otherUserID, int offset, int count) throws SQLException{ + return new PaginatedList<>(getByIdAsList(getMutualFriendIDsForUser(userID, otherUserID, offset, count)), getMutualFriendsCount(userID, otherUserID), offset, count); } - public static List getNonMutualFollowers(int userID, boolean followers, boolean accepted) throws SQLException{ + public static PaginatedList getNonMutualFollowers(int userID, boolean followers, boolean accepted, int offset, int count) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); String fld1=followers ? "follower_id" : "followee_id"; String fld2=followers ? "followee_id" : "follower_id"; - PreparedStatement stmt=conn.prepareStatement("SELECT `"+fld1+"` FROM followings WHERE `"+fld2+"`=? AND `mutual`=0 AND `accepted`=?"); - stmt.setInt(1, userID); - stmt.setBoolean(2, accepted); + PreparedStatement stmt=new SQLQueryBuilder(conn) + .selectFrom("followings") + .count() + .where(fld2+"=? AND accepted=? AND mutual=0", userID, accepted) + .createStatement(); + int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery()); + if(total==0) + return PaginatedList.emptyList(count); + stmt=new SQLQueryBuilder(conn) + .selectFrom("followings") + .columns(fld1) + .where(fld2+"=? AND accepted=? AND mutual=0", userID, accepted) + .orderBy(fld1+" ASC") + .limit(count, offset) + .createStatement(); try(ResultSet res=stmt.executeQuery()){ - return getByIdAsList(DatabaseUtils.intResultSetToList(res)); + return new PaginatedList<>(getByIdAsList(DatabaseUtils.intResultSetToList(res)), total, offset, count); } } - public static List getIncomingFriendRequestsForUser(int userID, int offset, int count) throws SQLException{ + public static PaginatedList getIncomingFriendRequestsForUser(int userID, int offset, int count) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); - PreparedStatement stmt=conn.prepareStatement("SELECT message, from_user_id FROM `friend_requests` WHERE `to_user_id`=? LIMIT ?,?"); - stmt.setInt(1, userID); - stmt.setInt(2, offset); - stmt.setInt(3, count); + PreparedStatement stmt=new SQLQueryBuilder(conn) + .selectFrom("friend_requests") + .count() + .where("to_user_id=?", userID) + .createStatement(); + int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery()); + if(total==0) + return PaginatedList.emptyList(count); + stmt=new SQLQueryBuilder(conn) + .selectFrom("friend_requests") + .columns("message", "from_user_id") + .where("to_user_id=?", userID) + .orderBy("id DESC") + .limit(count, offset) + .createStatement(); List reqs; // 1. collect the IDs of mutual friends for each friend request Map> mutualFriendIDs=new HashMap<>(); @@ -358,9 +400,9 @@ public static List getIncomingFriendRequestsForUser(int userID, i }).collect(Collectors.toList()); } if(mutualFriendIDs.isEmpty()) - return reqs; + return new PaginatedList<>(reqs, total, offset, count); // 2. make a list of distinct users we need - List needUsers=mutualFriendIDs.values().stream().flatMap(Collection::stream).distinct().collect(Collectors.toList()); + Set needUsers=mutualFriendIDs.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()); // 3. get them all in one go Map mutualFriends=getById(needUsers); // 4. finally, put them into friend requests @@ -370,7 +412,7 @@ public static List getIncomingFriendRequestsForUser(int userID, i continue; req.mutualFriends=ids.stream().map(mutualFriends::get).collect(Collectors.toList()); } - return reqs; + return new PaginatedList<>(reqs, total, offset, count); } public static void acceptFriendRequest(int userID, int targetUserID, boolean followAccepted) throws SQLException{ diff --git a/src/main/java/smithereen/templates/LangDateFunction.java b/src/main/java/smithereen/templates/LangDateFunction.java index bc1f9f40..995af26b 100644 --- a/src/main/java/smithereen/templates/LangDateFunction.java +++ b/src/main/java/smithereen/templates/LangDateFunction.java @@ -4,6 +4,7 @@ import com.mitchellbosecke.pebble.template.EvaluationContext; import com.mitchellbosecke.pebble.template.PebbleTemplate; +import java.time.Instant; import java.time.LocalDate; import java.util.Collections; import java.util.Date; @@ -17,12 +18,12 @@ public class LangDateFunction implements Function{ @Override public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber){ Object arg=args.get("date"); - if(arg instanceof java.sql.Date) - return Lang.get(context.getLocale()).formatDay(((java.sql.Date) arg).toLocalDate()); - if(arg instanceof LocalDate) - return Lang.get(context.getLocale()).formatDay((LocalDate) arg); - if(arg instanceof Date) - return Lang.get(context.getLocale()).formatDate((Date) arg, (TimeZone) context.getVariable("timeZone"), (Boolean) args.getOrDefault("forceAbsolute", Boolean.FALSE)); + if(arg instanceof java.sql.Date sd) + return Lang.get(context.getLocale()).formatDay(sd.toLocalDate()); + if(arg instanceof LocalDate ld) + return Lang.get(context.getLocale()).formatDay(ld); + if(arg instanceof Instant instant) + return Lang.get(context.getLocale()).formatDate(instant, (TimeZone) context.getVariable("timeZone"), (Boolean) args.getOrDefault("forceAbsolute", Boolean.FALSE)); return "????"; } diff --git a/src/main/java/smithereen/templates/RenderedTemplateResponse.java b/src/main/java/smithereen/templates/RenderedTemplateResponse.java index 9848b4b5..e275cf4f 100644 --- a/src/main/java/smithereen/templates/RenderedTemplateResponse.java +++ b/src/main/java/smithereen/templates/RenderedTemplateResponse.java @@ -1,8 +1,13 @@ package smithereen.templates; +import com.mitchellbosecke.pebble.error.PebbleException; import com.mitchellbosecke.pebble.template.PebbleTemplate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; +import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; import java.util.ArrayList; @@ -11,10 +16,14 @@ import java.util.Locale; import smithereen.Utils; +import smithereen.data.PaginatedList; import spark.Request; import spark.Response; +import spark.utils.StringUtils; public class RenderedTemplateResponse{ + private static final Logger LOG=LoggerFactory.getLogger(RenderedTemplateResponse.class); + String templateName; final HashMap model=new HashMap<>(); private PebbleTemplate template; @@ -54,13 +63,35 @@ public RenderedTemplateResponse addNavBarItem(String title){ return addNavBarItem(title, null, null); } + public RenderedTemplateResponse paginate(PaginatedList list, String urlPrefix, String firstPageURL){ + model.put("items", list.list); + model.put("paginationOffset", list.offset); + model.put("paginationPerPage", list.perPage); + model.put("totalItems", list.total); + model.put("paginationUrlPrefix", urlPrefix); + if(StringUtils.isNotEmpty(firstPageURL)) + model.put("paginationFirstPageUrl", firstPageURL); + return this; + } + + public RenderedTemplateResponse paginate(PaginatedList list){ + return paginate(list, req.pathInfo()+"?offset=", req.pathInfo()); + } + public void setName(String name){ templateName=name; } public void renderToWriter(Writer writer) throws IOException{ - template=getAndPrepareTemplate(req); - template.evaluate(writer, model, locale); + try{ + template=getAndPrepareTemplate(req); + template.evaluate(writer, model, locale); + }catch(PebbleException x){ + writer.write("
");
+			x.printStackTrace(new PrintWriter(writer));
+			writer.write("
"); + LOG.error("Error rendering template {}", templateName, x); + } } public String renderToString(){ diff --git a/src/main/resources/langs/en.json b/src/main/resources/langs/en.json index 33af93b0..92cea05d 100644 --- a/src/main/resources/langs/en.json +++ b/src/main/resources/langs/en.json @@ -440,5 +440,23 @@ "summary_X_managed_groups": "You manage {numGroups, plural, one {# group} other {# groups}}", "editing_post": "Editing post", "editing_comment": "Editing comment", - "edit_poll_warning": "If you change anything in the poll, all votes will be reset." + "edit_poll_warning": "If you change anything in the poll, all votes will be reset.", + "create_group_title": "Create a new group", + "create_event": "Create an event", + "create_event_title": "Create a new event", + "event_start_time": "Start time", + "event_end_time": "End time", + "event_specify_end_time": "Specify an end time", + "group_description": "Group description", + "event_description": "Event description", + "menu_events": "My Events", + "events_upcoming": "Upcoming", + "events_past": "Past", + "events_calendar": "Calendar", + "summary_own_X_followers": "{count, plural, =0 {No one follows} one {# person follows} other {# people follow}} you", + "summary_user_X_followers": "{count, plural, =0 {No one follows} one {# person follows} other {# people follow}} {name}", + "summary_own_X_follows": "You {count, plural, =0 {don''t follow anyone} one {follow # person} other {follow # people}}", + "summary_user_X_follows": "{name} {count, plural, =0 {doesn''t follow anyone} one {follows # person} other {follows # people}}", + "no_followers": "There are no followers", + "no_follows": "There are no follows" } \ No newline at end of file diff --git a/src/main/resources/langs/ru.json b/src/main/resources/langs/ru.json index 18f4b289..5209b5b3 100644 --- a/src/main/resources/langs/ru.json +++ b/src/main/resources/langs/ru.json @@ -442,5 +442,23 @@ "summary_X_managed_groups": "Вы управляете {numGroups, plural, one {# группой} other {# группами}}", "editing_post": "Редактирование записи", "editing_comment": "Редактирование комментария", - "edit_poll_warning": "Если Вы измените опрос, все существующие голоса будут удалены." + "edit_poll_warning": "Если Вы измените опрос, все существующие голоса будут удалены.", + "create_group_title": "Создание новой группы", + "create_event": "Создать встречу", + "create_event_title": "Создание новой встречи", + "event_start_time": "Время начала", + "event_end_time": "Время окончания", + "event_specify_end_time": "Указать время окончания", + "group_description": "Описание группы", + "event_description": "Описание встречи", + "menu_events": "Мои Встречи", + "events_upcoming": "Грядущие", + "events_past": "Прошедшие", + "events_calendar": "Календарь", + "summary_own_X_followers": "{count, plural, =0 {Никто не подписался} one {# человек подписался} other {# человек подписались}} на Ваши обновления", + "summary_user_X_followers": "{count, plural, =0 {Никто не подписался} one {# человек подписался} other {# человек подписались}} на обновления {name, inflect, genitive}", + "summary_own_X_follows": "Вы {count, select, 0 {не подписались ни на чьи обновления} other {подписались на обновления {count, plural, one {# человека} other {# человек}}}}", + "summary_user_X_follows": "{name} {count, select, 0 {не {gender, select, female {подписалась} other {подписался}} ни на чьи обновления} other {{gender, select, female {подписалась} other {подписался}} на обновления {count, plural, one {# человека} other {# человек}}}}", + "no_followers": "Нет ни одного подписчика", + "no_follows": "Нет ни одной подписки" } \ No newline at end of file diff --git a/src/main/resources/templates/common/create_group.twig b/src/main/resources/templates/common/create_group.twig index b9a8dba0..967227bd 100644 --- a/src/main/resources/templates/common/create_group.twig +++ b/src/main/resources/templates/common/create_group.twig @@ -1,5 +1,5 @@ {% import "forms" as form %} {{ form.start('createGroup', createGroupMessage) }} {{ form.textInput('name', L('group_name'), groupName, {'maxlength': '200', 'required': true, 'autocomplete': false}) }} - {{ form.textInput('username', L('group_username'), groupUsername, {'maxlength': 50, 'required': true, 'pattern': '^[a-zA-Z][a-zA-Z0-9._-]+$', 'autocomplete': false, 'explanation': L('username_explain'), 'prefix': "#{serverDomain}/"}) }} + {{ form.textArea('description', L('group_description'), groupDescription) }} {{ form.end() }} \ No newline at end of file diff --git a/src/main/resources/templates/common/friends_tabbar.twig b/src/main/resources/templates/common/friends_tabbar.twig index d2790348..f194222a 100644 --- a/src/main/resources/templates/common/friends_tabbar.twig +++ b/src/main/resources/templates/common/friends_tabbar.twig @@ -3,8 +3,8 @@ {% if mutualCount>0 %} {{L('X_mutual_friends_short', {'count': mutualCount})}} {% endif %} - {{L('followers')}} - {{L('following')}} + {{L('followers')}} + {{L('following')}} {%if currentUser is not null and currentUser.id==owner.id%} {%if userNotifications.newFriendRequestCount>0%} {{ L('friend_requests')}} ({{ userNotifications.newFriendRequestCount }}) diff --git a/src/main/resources/templates/common/left_menu.twig b/src/main/resources/templates/common/left_menu.twig new file mode 100644 index 00000000..cd2b264f --- /dev/null +++ b/src/main/resources/templates/common/left_menu.twig @@ -0,0 +1,33 @@ + +{% if birthdayUsers is not empty %} +
+

{{ L('reminder') }}

+ {{ L('birthday_reminder_before') -}} + {{- L(birthdaysAreToday ? 'reminder_today' : 'reminder_tomorrow') -}} + {{- L('birthday_reminder_middle') -}} + {%- for user in birthdayUsers -%} + {{ L('birthday_reminder_name', {'name': user.firstLastAndGender}) }} + {%- if not loop.last -%} + {{- L(loop.revindex>1 ? 'birthday_reminder_separator' : 'birthday_reminder_separator_last') -}} + {%- endif -%} + {%- endfor -%} + {{- L('birthday_reminder_after') -}} +
+{% endif %} diff --git a/src/main/resources/templates/common/pagination.twig b/src/main/resources/templates/common/pagination.twig new file mode 100644 index 00000000..b751442e --- /dev/null +++ b/src/main/resources/templates/common/pagination.twig @@ -0,0 +1,21 @@ +{# @pebvariable name="paginationOffset" type="int" #} +{# @pebvariable name="paginationPerPage" type="int" #} +{# @pebvariable name="totalItems" type="int" #} +{# @pebvariable name="paginationUrlPrefix" type="String" #} +{# @pebvariable name="paginationFirstPageUrl" type="String" #} +{# @pebvariable name="paginationAjax" type="boolean" #} +{% set curPage=paginationOffset/paginationPerPage %} +{% set totalPages=(totalItems+paginationPerPage-1)/paginationPerPage %} +{#- << 3 4 _5_ 6 7 >> -#} +{#- 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/common/wall_tabbar.twig b/src/main/resources/templates/common/wall_tabbar.twig index d30c4e08..56034fb3 100644 --- a/src/main/resources/templates/common/wall_tabbar.twig +++ b/src/main/resources/templates/common/wall_tabbar.twig @@ -3,9 +3,9 @@ {# @pebvariable name="isGroup" type="boolean" #} {# @pebvariable name="tab" type="String" #}
- {{ L(isGroup ? 'wall_of_group' : 'wall_all_posts') }} + {{ L(isGroup ? 'wall_of_group' : 'wall_all_posts') }} {% if not isGroup %} - {{ owner==currentUser ? L('wall_my_posts') : L('wall_posts_of_X', {'name': owner.firstAndGender}) }} + {{ owner==currentUser ? L('wall_my_posts') : L('wall_posts_of_X', {'name': owner.firstAndGender}) }} {% endif %} {% if tab=="single" %} {{ L('wall_post_title') }} diff --git a/src/main/resources/templates/desktop/feed.twig b/src/main/resources/templates/desktop/feed.twig index 1a37227b..f916bdba 100644 --- a/src/main/resources/templates/desktop/feed.twig +++ b/src/main/resources/templates/desktop/feed.twig @@ -4,7 +4,7 @@ {%include "wall_post_form" with {'id': "feed"}%}
{{ L('summary_feed') }}
- {%include "pagination" with {'perPage': 25, 'offset': offset, 'total': total, 'urlPrefix': paginationURL, 'firstPageURL': paginationFirstURL | default("/feed") }%} + {% include "pagination" %}
{%for entry in feed%} @@ -52,9 +52,7 @@ Unknown entry type {{entry.type}}
{{ L('feed_empty') }}
{%endfor%}
-{% if total>25 %}
- {%include "pagination" with {'perPage': 25, 'offset': offset, 'total': total, 'urlPrefix': paginationURL, 'firstPageURL': paginationFirstURL | default("/feed") }%} + {% include "pagination" %}
-{% endif %} {%endblock%} diff --git a/src/main/resources/templates/desktop/friend_requests.twig b/src/main/resources/templates/desktop/friend_requests.twig index 67c970bc..1f32279a 100644 --- a/src/main/resources/templates/desktop/friend_requests.twig +++ b/src/main/resources/templates/desktop/friend_requests.twig @@ -1,12 +1,13 @@ -{# @pebvariable name="friendRequests" type="java.util.List" #} +{# @pebvariable name="items" type="java.util.List" #} {%extends "page"%} {%block content%} {% include 'friends_tabbar' with {'tab': 'requests'} %}
-
{{ L('summary_X_friend_requests', {'numRequests': userNotifications.newFriendRequestCount}) }}
+
{{ L('summary_X_friend_requests', {'numRequests': totalItems}) }}
+ {% include "pagination" %}
- {% for req in friendRequests %} + {% for req in items %}
@@ -64,4 +65,5 @@
{{L('no_incoming_friend_requests')}}
{% endfor %} +
{% include "pagination" %}
{%endblock%} \ No newline at end of file diff --git a/src/main/resources/templates/desktop/friends.twig b/src/main/resources/templates/desktop/friends.twig index c36b5284..eca1c2fe 100644 --- a/src/main/resources/templates/desktop/friends.twig +++ b/src/main/resources/templates/desktop/friends.twig @@ -6,19 +6,33 @@ {% include 'friends_tabbar' %}
+ {% set isSelf=currentUser is not null and currentUser.id==owner.id %} {% if tab=='mutual' %} - {{ L('you_and_X_mutual', {'name': owner.firstAndGender, 'numFriends': friendList.size()}) }} + {{ L('you_and_X_mutual', {'name': owner.firstAndGender, 'numFriends': totalItems}) }} + {% elseif tab=='followers' %} + {% if isSelf %} + {{ L("summary_own_X_followers", {'count': totalItems}) }} + {% else %} + {{ L("summary_user_X_followers", {'name': owner.firstAndGender, 'count': totalItems}) }} + {% endif %} + {% elseif tab=='following' %} + {% if isSelf %} + {{ L("summary_own_X_follows", {'count': totalItems}) }} + {% else %} + {{ L("summary_user_X_follows", {'name': owner.firstAndGender, 'count': totalItems}) }} + {% endif %} {% else %} - {% if currentUser is not null and currentUser.id==owner.id %} - {{ L("summary_own_X_friends", {'numFriends': friendList.size()}) }} - {% else %} - {{ L("summary_user_X_friends", {'name': owner.firstAndGender, 'numFriends': friendList.size()}) }} - {% endif %} + {% if isSelf %} + {{ L("summary_own_X_friends", {'numFriends': totalItems}) }} + {% else %} + {{ L("summary_user_X_friends", {'name': owner.firstAndGender, 'numFriends': totalItems}) }} + {% endif %} {% endif %}
+ {% include "pagination" %}
-{%for friend in friendList%} +{% for friend in items %}
@@ -53,8 +67,17 @@
-{%else%} -
{{ L('no_friends') }}
-{%endfor%} +{% else %} +
+ {% if tab=='followers' %} + {{ L('no_followers') }} + {% elseif tab=='following' %} + {{ L('no_follows') }} + {% else %} + {{ L('no_friends') }} + {% endif %} +
+{% endfor %}
+
{% include "pagination" %}
{%endblock%} \ No newline at end of file diff --git a/src/main/resources/templates/desktop/group.twig b/src/main/resources/templates/desktop/group.twig index 79c3ab19..59182e09 100644 --- a/src/main/resources/templates/desktop/group.twig +++ b/src/main/resources/templates/desktop/group.twig @@ -23,7 +23,7 @@ {% endif %}
- {% include 'wall_profile_block' with {'wallOwner': group, 'isGroup': true, 'fullWallURL': "#{group.profileURL}/wall"} %} + {% include 'wall_profile_block' with {'wallOwner': group, 'isGroup': true, 'fullWallURL': "/groups/#{group.id}/wall"} %} diff --git a/src/main/resources/templates/desktop/group_edit_members.twig b/src/main/resources/templates/desktop/group_edit_members.twig index 22d24ed2..602c47a3 100644 --- a/src/main/resources/templates/desktop/group_edit_members.twig +++ b/src/main/resources/templates/desktop/group_edit_members.twig @@ -7,7 +7,7 @@ {% include "group_admin_tabbar" with {'tab': 'members'} %}
- {% for member in members %} + {% for member in items %}
{{ member | pictureForAvatar('s', 30) }}
@@ -25,9 +25,5 @@ {% endfor %}
-{% if total>100 %} -
-{%include "pagination" with {'perPage': 100, 'offset': pageOffset, 'total': total, 'urlPrefix': paginationUrlPrefix}%} -
-{% endif %} +
{% include "pagination" %}
{% endblock %} \ No newline at end of file diff --git a/src/main/resources/templates/desktop/groups.twig b/src/main/resources/templates/desktop/groups.twig index 2c85ceb7..7853ac69 100644 --- a/src/main/resources/templates/desktop/groups.twig +++ b/src/main/resources/templates/desktop/groups.twig @@ -11,14 +11,15 @@
{% if currentUser is not null and owner.id==currentUser.id %} - {{ L(tab=='managed' ? 'summary_X_managed_groups' : 'summary_own_X_groups', {'numGroups': groups.size()}) }} + {{ L(tab=='managed' ? 'summary_X_managed_groups' : 'summary_own_X_groups', {'numGroups': totalItems}) }} {% else %} - {{ L('summary_user_X_groups', {'name': owner.firstAndGender, 'numGroups': groups.size()}) }} + {{ L('summary_user_X_groups', {'name': owner.firstAndGender, 'numGroups': totalItems}) }} {% endif %}
+ {% include "pagination" %}
-{% for group in groups %} +{% for group in items %}
@@ -40,4 +41,5 @@
{{L('no_groups')}}
{% endfor %} +
{% include "pagination" %}
{% endblock %} \ No newline at end of file diff --git a/src/main/resources/templates/desktop/notifications.twig b/src/main/resources/templates/desktop/notifications.twig index c8be077f..b6afd1e6 100644 --- a/src/main/resources/templates/desktop/notifications.twig +++ b/src/main/resources/templates/desktop/notifications.twig @@ -5,10 +5,10 @@ {%else%}
{{ L('summary_notifications') }}
- {%include "pagination" with {'perPage': 50, 'offset': offset, 'total': total, 'urlPrefix': "/my/notifications?offset="}%} + {% include "pagination" %}
-{%for notification in notifications%} +{%for notification in items%} {%set actor=users[notification.actorID]%} {%if notification.objectID!=0%} {%set object=posts[notification.objectID]%} @@ -88,10 +88,8 @@
{{ L('notifications_empty') }}
{%endfor%}
-{% if total>50 %}
- {%include "pagination" with {'perPage': 50, 'offset': offset, 'total': total, 'urlPrefix': "/my/notifications?offset="}%} + {% include "pagination" %}
-{% endif %} {%endif%} {%endblock%} \ No newline at end of file diff --git a/src/main/resources/templates/desktop/page.twig b/src/main/resources/templates/desktop/page.twig index fe54decf..d3b3dd1b 100644 --- a/src/main/resources/templates/desktop/page.twig +++ b/src/main/resources/templates/desktop/page.twig @@ -57,33 +57,7 @@
{%block leftMenu%} {%if currentUser is not null%} - - {% if birthdayUsers is not empty %} -
-

{{ L('reminder') }}

- {{ L('birthday_reminder_before') -}} - {{- L(birthdaysAreToday ? 'reminder_today' : 'reminder_tomorrow') -}} - {{- L('birthday_reminder_middle') -}} - {%- for user in birthdayUsers -%} - {{ L('birthday_reminder_name', {'name': user.firstLastAndGender}) }} - {%- if not loop.last -%} - {{- L(loop.revindex>1 ? 'birthday_reminder_separator' : 'birthday_reminder_separator_last') -}} - {%- endif -%} - {%- endfor -%} - {{- L('birthday_reminder_after') -}} -
- {% endif %} + {% include "left_menu" %} {%else%}
{{ L("email_or_username") }}:
diff --git a/src/main/resources/templates/desktop/pagination.twig b/src/main/resources/templates/desktop/pagination.twig deleted file mode 100644 index c9af378d..00000000 --- a/src/main/resources/templates/desktop/pagination.twig +++ /dev/null @@ -1,15 +0,0 @@ -{%set curPage=offset/perPage%} -{%set totalPages=(total+perPage-1)/perPage%} -{#- << 3 4 _5_ 6 7 >> -#} -{#- 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/profile.twig b/src/main/resources/templates/desktop/profile.twig index e8f2b6fe..5391f1c1 100644 --- a/src/main/resources/templates/desktop/profile.twig +++ b/src/main/resources/templates/desktop/profile.twig @@ -62,8 +62,8 @@
  • {{ L('block_user_X', {'name': user.firstAndGender}) }}
  • {% endif %} {%endif%} -
  • {{L('followers')}}
  • -
  • {{L('following')}}
  • +
  • {{L('followers')}}
  • +
  • {{L('following')}}
  • @@ -134,7 +134,7 @@ {% endfor %}
    - {% include "wall_profile_block" with {'wallOwner': user, 'fullWallURL': "#{user.profileURL}/wall"} %} + {% include "wall_profile_block" with {'wallOwner': user, 'fullWallURL': "/users/#{user.id}/wall"} %} diff --git a/src/main/resources/templates/desktop/user_grid.twig b/src/main/resources/templates/desktop/user_grid.twig index 2fa33100..5d62b5c9 100644 --- a/src/main/resources/templates/desktop/user_grid.twig +++ b/src/main/resources/templates/desktop/user_grid.twig @@ -1,10 +1,10 @@ -{% if users is not empty %} +{% if items is not empty %}
    {{ summary }}
    - {%include "pagination" with {'perPage': 100, 'offset': pageOffset, 'total': total, 'urlPrefix': paginationUrlPrefix, 'ajax': true}%} + {% include "pagination" %}
    - {%for user in users%} + {%for user in items%} {%endfor%}
    -{% if total>100 %} -
    - {%include "pagination" with {'perPage': 100, 'offset': pageOffset, 'total': total, 'urlPrefix': paginationUrlPrefix, 'ajax': true}%} -
    -{% endif %} +
    {% include "pagination" %}
    {% else %}
    diff --git a/src/main/resources/templates/desktop/wall_page.twig b/src/main/resources/templates/desktop/wall_page.twig index c6499353..7bcfa68b 100644 --- a/src/main/resources/templates/desktop/wall_page.twig +++ b/src/main/resources/templates/desktop/wall_page.twig @@ -2,19 +2,15 @@ {% block content %} {% include 'wall_tabbar' %}
    -
    {{ L('total_X_posts', {'count': postCount}) }}
    - {%include "pagination" with {'perPage': 25, 'offset': pageOffset, 'total': postCount, 'urlPrefix': (paginationUrlPrefix+"?offset=")}%} +
    {{ L('total_X_posts', {'count': totalItems}) }}
    + {% include "pagination" %}
    -{% for post in posts %} +{% for post in items %}
    {% include 'wall_post' %}
    {% else %}
    {{ L('wall_empty') }}
    {% endfor %}
    -{% if postCount>25 %} -
    - {%include "pagination" with {'perPage': 25, 'offset': pageOffset, 'total': postCount, 'urlPrefix': (paginationUrlPrefix+"?offset=")}%} -
    -{% endif %} +
    {% include "pagination" %}
    {% endblock %} diff --git a/src/main/resources/templates/desktop/wall_post.twig b/src/main/resources/templates/desktop/wall_post.twig index 79b7c80b..fcd22ddb 100644 --- a/src/main/resources/templates/desktop/wall_post.twig +++ b/src/main/resources/templates/desktop/wall_post.twig @@ -41,7 +41,7 @@
    {{ L('edit') }} {% endif %} {% if post.owner.id!=post.user.id and tab!="wall2wall" and not post.isGroupOwner %} | - {{ L('wall_to_wall') }} + {{ L('wall_to_wall') }} {% endif %} {% if not post.local %} | {{ L('open_on_server_X', {'domain': post.url.host}) }} diff --git a/src/main/resources/templates/desktop/wall_profile_block.twig b/src/main/resources/templates/desktop/wall_profile_block.twig index e620bae1..fe46703b 100644 --- a/src/main/resources/templates/desktop/wall_profile_block.twig +++ b/src/main/resources/templates/desktop/wall_profile_block.twig @@ -7,7 +7,7 @@ {%endif%} - {%for post in wall%} + {%for post in items%}
    {%include "wall_post" with {'post': post}%}
    {% else %}
    {{ L('wall_empty') }}
    @@ -15,7 +15,7 @@
    - {%include "pagination" with {'perPage': 25, 'offset': pageOffset, 'total': postCount, 'urlPrefix': (user.profileURL+"?offset=")}%} + {% include "pagination" %}
    diff --git a/src/main/resources/templates/mobile/feed.twig b/src/main/resources/templates/mobile/feed.twig index c3f5bad6..7c72a002 100644 --- a/src/main/resources/templates/mobile/feed.twig +++ b/src/main/resources/templates/mobile/feed.twig @@ -50,7 +50,7 @@ Unknown entry type {{entry.type}} {% else %}
    {{ L('feed_empty') }}
    {%endfor%} - {%include "pagination" with {'perPage': 25, 'offset': offset, 'total': total, 'urlPrefix': paginationURL, 'firstPageURL': paginationFirstURL | default("/feed") }%} + {% include "pagination" %}
    {%endblock%} diff --git a/src/main/resources/templates/mobile/friend_requests.twig b/src/main/resources/templates/mobile/friend_requests.twig index 5b678d55..ce85e1e7 100644 --- a/src/main/resources/templates/mobile/friend_requests.twig +++ b/src/main/resources/templates/mobile/friend_requests.twig @@ -1,9 +1,9 @@ -{# @pebvariable name="friendRequests" type="java.util.List" #} +{# @pebvariable name="items" type="java.util.List" #} {%extends "page"%} {%block content%} {% include 'friends_tabbar' with {'tab': 'requests'} %}
    - {% for req in friendRequests %} + {% for req in items %} @@ -76,43 +76,8 @@
    -
    @@ -28,4 +28,5 @@
    {{L('no_incoming_friend_requests')}}
    {% endfor %} +{% include "pagination" %} {%endblock%} \ No newline at end of file diff --git a/src/main/resources/templates/mobile/friends.twig b/src/main/resources/templates/mobile/friends.twig index 4bc9867e..11418fbd 100644 --- a/src/main/resources/templates/mobile/friends.twig +++ b/src/main/resources/templates/mobile/friends.twig @@ -2,7 +2,7 @@ {%block content%} {% include 'friends_tabbar' %}
    -{%for friend in friendList%} +{% for friend in items %}
    @@ -13,8 +13,17 @@
    -{%else%} -
    {{L('no_friends')}}
    -{%endfor%} +{% else %} +
    + {% if tab=='followers' %} + {{ L('no_followers') }} + {% elseif tab=='following' %} + {{ L('no_follows') }} + {% else %} + {{ L('no_friends') }} + {% endif %}
    +{% endfor %} +
    +{% include "pagination" %} {%endblock%} \ No newline at end of file diff --git a/src/main/resources/templates/mobile/group.twig b/src/main/resources/templates/mobile/group.twig index a80c28de..df5f11e1 100644 --- a/src/main/resources/templates/mobile/group.twig +++ b/src/main/resources/templates/mobile/group.twig @@ -4,11 +4,11 @@ {% set canEditGroup=userPermissions is not null and userPermissions.canEditGroup(group) %} {% set canManageGroup=userPermissions is not null and userPermissions.canManageGroup(group) %} {%block content%} -{%if user.domain is not null%}
    +{%if group.domain is not null%} {%endif%}
    diff --git a/src/main/resources/templates/mobile/group_edit_members.twig b/src/main/resources/templates/mobile/group_edit_members.twig index b3dad9ce..cf52b526 100644 --- a/src/main/resources/templates/mobile/group_edit_members.twig +++ b/src/main/resources/templates/mobile/group_edit_members.twig @@ -6,7 +6,7 @@ {% block content %} {% include "group_admin_tabbar" with {'tab': 'members'} %}
    - {% for member in members %} + {% for member in items %}
    {{ member | pictureForAvatar('s') }} {% if not (adminIDs contains member.id) %} @@ -24,5 +24,5 @@
    {% endfor %}
    -{%include "pagination" with {'perPage': 100, 'offset': pageOffset, 'total': total, 'urlPrefix': paginationUrlPrefix}%} +{% include "pagination" %} {% endblock %} \ No newline at end of file diff --git a/src/main/resources/templates/mobile/groups.twig b/src/main/resources/templates/mobile/groups.twig index a6468536..6d91567c 100644 --- a/src/main/resources/templates/mobile/groups.twig +++ b/src/main/resources/templates/mobile/groups.twig @@ -4,7 +4,7 @@ {% include 'groups_tabbar' %} {% endif %}
    -{% for group in groups %} +{% for group in items %}
    @@ -18,5 +18,6 @@ {%else%}
    {{ L('no_groups') }}
    {%endfor%} +{% include "pagination" %} {%endblock%} \ No newline at end of file diff --git a/src/main/resources/templates/mobile/notifications.twig b/src/main/resources/templates/mobile/notifications.twig index 0d787dd6..70b48003 100644 --- a/src/main/resources/templates/mobile/notifications.twig +++ b/src/main/resources/templates/mobile/notifications.twig @@ -1,10 +1,7 @@ {%extends "page"%} {%block content%} -{%if notifications.empty%} -
    {{L('no_notifications')}}
    -{%else%}
    -{%for notification in notifications%} +{%for notification in items%} {%set actor=users[notification.actorID]%} {%if notification.objectID!=0%} {%set object=posts[notification.objectID]%} @@ -83,7 +80,6 @@ {% else %}
    {{ L('notifications_empty') }}
    {%endfor%} - {%include "pagination" with {'perPage': 50, 'offset': offset, 'total': total, 'urlPrefix': "/my/notifications?offset="}%} +{% include "pagination" %}
    -{%endif%} {%endblock%} \ No newline at end of file diff --git a/src/main/resources/templates/mobile/page.twig b/src/main/resources/templates/mobile/page.twig index 30810207..a01d5703 100644 --- a/src/main/resources/templates/mobile/page.twig +++ b/src/main/resources/templates/mobile/page.twig @@ -47,40 +47,7 @@ {% endif %} {%block leftMenu%} - -{% if birthdayUsers is not empty %} -
    -

    {{ L('reminder') }}

    - {{ L('birthday_reminder_before') -}} - {{- L(birthdaysAreToday ? 'reminder_today' : 'reminder_tomorrow') -}} - {{- L('birthday_reminder_middle') -}} - {%- for user in birthdayUsers -%} - {{ L('birthday_reminder_name', {'name': user.firstLastAndGender}) }} - {%- if not loop.last -%} - {{- L(loop.revindex>1 ? 'birthday_reminder_separator' : 'birthday_reminder_separator_last') -}} - {%- endif -%} - {%- endfor -%} - {{- L('birthday_reminder_after') -}} -
    -{% endif %} +{% include "left_menu" %} {%endblock%} diff --git a/src/main/resources/templates/mobile/pagination.twig b/src/main/resources/templates/mobile/pagination.twig deleted file mode 100644 index c9af378d..00000000 --- a/src/main/resources/templates/mobile/pagination.twig +++ /dev/null @@ -1,15 +0,0 @@ -{%set curPage=offset/perPage%} -{%set totalPages=(total+perPage-1)/perPage%} -{#- << 3 4 _5_ 6 7 >> -#} -{#- 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/mobile/profile.twig b/src/main/resources/templates/mobile/profile.twig index 354adc27..e093a2a4 100644 --- a/src/main/resources/templates/mobile/profile.twig +++ b/src/main/resources/templates/mobile/profile.twig @@ -58,8 +58,8 @@
  • {{ L('block_user_X', {'name': user.firstAndGender}) }}
  • {% endif %} {%endif%} -
  • {{L('followers')}}
  • -
  • {{L('following')}}
  • +
  • {{L('followers')}}
  • +
  • {{L('following')}}
  • {% if groups is not empty %}
  • {{ L('X_groups', {'count': groups.size}) }}
  • {% endif %} diff --git a/src/main/resources/templates/mobile/user_grid.twig b/src/main/resources/templates/mobile/user_grid.twig index 29faa16a..e1607624 100644 --- a/src/main/resources/templates/mobile/user_grid.twig +++ b/src/main/resources/templates/mobile/user_grid.twig @@ -1,5 +1,5 @@
    - {%for user in users%} + {% for user in items %}
    @@ -12,6 +12,6 @@
    {% else %}
    {{ emptyMessage }}
    - {%endfor%} - {%include "pagination" with {'perPage': 100, 'offset': pageOffset, 'total': total, 'urlPrefix': paginationUrlPrefix, 'ajax': true}%} + {% endfor %} + {% include "pagination" %}
    \ No newline at end of file diff --git a/src/main/resources/templates/mobile/wall_page.twig b/src/main/resources/templates/mobile/wall_page.twig index 848c0da8..5a80f729 100644 --- a/src/main/resources/templates/mobile/wall_page.twig +++ b/src/main/resources/templates/mobile/wall_page.twig @@ -2,11 +2,11 @@ {% block content %} {% include 'wall_tabbar' %}
    -{% for post in posts %} +{% for post in items %}
    {% include 'wall_post' %}
    {% endfor %} -{%include "pagination" with {'perPage': 25, 'offset': pageOffset, 'total': postCount, 'urlPrefix': (paginationUrlPrefix+"?offset=")}%} +{% include "pagination" %}
    {% endblock %} \ No newline at end of file diff --git a/src/main/resources/templates/mobile/wall_post_standalone.twig b/src/main/resources/templates/mobile/wall_post_standalone.twig index 94a2b1f0..952c7531 100644 --- a/src/main/resources/templates/mobile/wall_post_standalone.twig +++ b/src/main/resources/templates/mobile/wall_post_standalone.twig @@ -33,7 +33,7 @@
  • Top-level
  • {%endif%} {% if post.owner.id!=post.user.id %} -
  • {{ L('wall_to_wall') }}
  • +
  • {{ L('wall_to_wall') }}
  • {% endif %}
    diff --git a/src/main/resources/templates/mobile/wall_profile_block.twig b/src/main/resources/templates/mobile/wall_profile_block.twig index fc72eb1f..90bfb235 100644 --- a/src/main/resources/templates/mobile/wall_profile_block.twig +++ b/src/main/resources/templates/mobile/wall_profile_block.twig @@ -4,16 +4,16 @@
    {%endif%}
    - {%for post in wall%} + {%for post in items%}
    {%include "wall_post" with {'post': post}%}
    {% else %} -
    +
    {{ L('wall_empty') }}
    {%endfor%}
    - {%include "pagination" with {'perPage': 25, 'offset': pageOffset, 'total': postCount, 'urlPrefix': (user.profileURL+"?offset=")}%} + {% include "pagination" %}
    diff --git a/src/main/web/common.scss b/src/main/web/common.scss index f66cb51c..8632fcbf 100644 --- a/src/main/web/common.scss +++ b/src/main/web/common.scss @@ -1,5 +1,4 @@ -$accent: #aab42f; $textOnAccent: #FFFFFF; $bg: #FFFFFF; $gray: #F7F7F7; diff --git a/src/main/web/desktop.scss b/src/main/web/desktop.scss index 2ab336be..fedd0391 100644 --- a/src/main/web/desktop.scss +++ b/src/main/web/desktop.scss @@ -1090,6 +1090,10 @@ select{ } } +.bottomSummaryWrap:empty{ + display: none; +} + .bottomSummaryWrap .pagination{ margin-top: -1px; a, span{ diff --git a/src/main/web/mobile.scss b/src/main/web/mobile.scss index b974bbd1..00735eba 100644 --- a/src/main/web/mobile.scss +++ b/src/main/web/mobile.scss @@ -89,7 +89,7 @@ h2{ font-size: 20px; margin: 0 -$contentPadding; padding: 0 8px 2px 8px; - border-bottom: solid 1px color($accent lightness(+30%)); + border-bottom: solid 1px $mainHeaderSeparator; } h3{ @@ -97,7 +97,7 @@ h3{ font-weight: bold; margin: 0 -$contentPadding; padding: 0 8px 2px 8px; - border-bottom: solid 1px color($accent lightness(+30%)); + border-bottom: solid 1px $mainHeaderSeparator; } h4{ @@ -333,20 +333,12 @@ select{ text-decoration: none; } .curPage{ - background: $accent; - color: $textOnAccent; + box-shadow: 0 1px 0 $normalLink inset, 0 4px 0 $tabBackground inset; + color: $normalLink; + font-weight: bold; } .page{ - border-bottom: solid 1px color($accent alpha(30%)); - padding-bottom: 7px; - transition-property: border-bottom-color, border-bottom-width, padding-bottom; - transition-duration: 0.2s; - transition-timing-function: ease-in-out; - &:hover, &:visited:hover{ - border-bottom-color: color($accent alpha(70%)); - border-bottom-width: 3px; - padding-bottom: 5px; - } + box-shadow: 0 1px 0 $wallPostSeparator inset; } } @@ -376,7 +368,7 @@ select{ } &.selected{ padding-bottom: 0; - border-bottom: solid 4px $accent; + border-bottom: solid 4px $tabBackground; } } } From 82a615979d6904c929bb3933b9f279b02fe5f63c Mon Sep 17 00:00:00 2001 From: Grishka Date: Sat, 20 Nov 2021 22:18:27 +0300 Subject: [PATCH 06/65] fix --- src/main/java/smithereen/storage/PostStorage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/smithereen/storage/PostStorage.java b/src/main/java/smithereen/storage/PostStorage.java index 04d6d5e3..66e456c8 100644 --- a/src/main/java/smithereen/storage/PostStorage.java +++ b/src/main/java/smithereen/storage/PostStorage.java @@ -1099,7 +1099,7 @@ public static PaginatedList getCommentsFeed(int userID, int offse } } - return new PaginatedList<>(entries, total); + return new PaginatedList<>(entries.stream().filter(e->!(e instanceof PostNewsfeedEntry) || (e instanceof PostNewsfeedEntry pe && pe.post!=null)).collect(Collectors.toList()), total, offset, count); } private static class DeleteCommentBookmarksRunnable implements Runnable{ From 42402566013eaae88a756f99a0b23db6e777065c Mon Sep 17 00:00:00 2001 From: Grishka Date: Sun, 21 Nov 2021 16:25:55 +0300 Subject: [PATCH 07/65] Refactor more things --- .../controllers/GroupsController.java | 43 ++++++++++++- .../controllers/WallController.java | 2 +- .../java/smithereen/routes/GroupsRoutes.java | 64 ++++++++----------- .../resources/templates/desktop/group.twig | 41 +----------- .../resources/templates/desktop/page.twig | 1 - .../resources/templates/desktop/profile.twig | 52 ++------------- .../templates/desktop/profile_module.twig | 17 +++++ .../desktop/profile_module_groups.twig | 8 +++ .../desktop/profile_module_user_grid.twig | 20 ++++++ .../desktop/profile_module_user_list.twig | 19 ++++++ .../desktop/profile_module_wall.twig | 14 ++++ .../templates/desktop/wall_profile_block.twig | 21 ------ src/main/resources/templates/mobile/page.twig | 1 - src/main/web/common_ts/Helpers.ts | 4 +- src/main/web/desktop.scss | 14 +++- 15 files changed, 170 insertions(+), 151 deletions(-) create mode 100644 src/main/resources/templates/desktop/profile_module.twig create mode 100644 src/main/resources/templates/desktop/profile_module_groups.twig create mode 100644 src/main/resources/templates/desktop/profile_module_user_grid.twig create mode 100644 src/main/resources/templates/desktop/profile_module_user_list.twig create mode 100644 src/main/resources/templates/desktop/profile_module_wall.twig delete mode 100644 src/main/resources/templates/desktop/wall_profile_block.twig diff --git a/src/main/java/smithereen/controllers/GroupsController.java b/src/main/java/smithereen/controllers/GroupsController.java index f33d2567..48940a2d 100644 --- a/src/main/java/smithereen/controllers/GroupsController.java +++ b/src/main/java/smithereen/controllers/GroupsController.java @@ -15,10 +15,12 @@ import smithereen.activitypub.ActivityPubWorker; import smithereen.data.ForeignGroup; import smithereen.data.Group; +import smithereen.data.GroupAdmin; import smithereen.data.PaginatedList; import smithereen.data.User; import smithereen.exceptions.InternalServerErrorException; import smithereen.exceptions.ObjectNotFoundException; +import smithereen.exceptions.UserActionNotAllowedException; import smithereen.storage.GroupStorage; import spark.utils.StringUtils; @@ -89,11 +91,50 @@ public Group getLocalGroupOrThrow(int id){ return group; } - public PaginatedList getMembers(Group group, int offset, int count){ + public PaginatedList getMembers(@NotNull Group group, int offset, int count){ try{ return GroupStorage.getMembers(group.id, offset, count); }catch(SQLException x){ throw new InternalServerErrorException(x); } } + + public List getAdmins(@NotNull Group group){ + try{ + return GroupStorage.getGroupAdmins(group.id); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public List getRandomMembersForProfile(@NotNull Group group){ + try{ + return GroupStorage.getRandomMembersForProfile(group.id); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + @NotNull + public Group.AdminLevel getMemberAdminLevel(@NotNull Group group, @NotNull User user){ + try{ + return GroupStorage.getGroupMemberAdminLevel(group.id, user.id); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + @NotNull + public Group.MembershipState getUserMembershipState(@NotNull Group group, @NotNull User user){ + try{ + return GroupStorage.getUserMembershipState(group.id, user.id); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public void enforceUserAdminLevel(@NotNull Group group, @NotNull User user, @NotNull Group.AdminLevel atLeastLevel){ + if(!getMemberAdminLevel(group, user).isAtLeast(atLeastLevel)) + throw new UserActionNotAllowedException(); + } } diff --git a/src/main/java/smithereen/controllers/WallController.java b/src/main/java/smithereen/controllers/WallController.java index 7c3fa551..5a144ac2 100644 --- a/src/main/java/smithereen/controllers/WallController.java +++ b/src/main/java/smithereen/controllers/WallController.java @@ -85,7 +85,7 @@ public Post createWallPost(@NotNull User author, @NotNull Actor wallOwner, int i if(textSource.length()==0 && attachmentIDs.isEmpty() && poll==null) throw new BadRequestException("Empty post"); - if(!wallOwner.hasWall()) + if(!wallOwner.hasWall() && inReplyToID==0) throw new BadRequestException("This actor doesn't support wall posts"); Post parent=inReplyToID!=0 ? getPostOrThrow(inReplyToID) : null; diff --git a/src/main/java/smithereen/routes/GroupsRoutes.java b/src/main/java/smithereen/routes/GroupsRoutes.java index 4c674d0e..8b9cb4de 100644 --- a/src/main/java/smithereen/routes/GroupsRoutes.java +++ b/src/main/java/smithereen/routes/GroupsRoutes.java @@ -53,11 +53,9 @@ private static Group getGroup(Request req){ return context(req).getGroupsController().getGroupOrThrow(id); } - private static Group getGroupAndRequireLevel(Request req, Account self, Group.AdminLevel level) throws SQLException{ + private static Group getGroupAndRequireLevel(Request req, Account self, Group.AdminLevel level){ Group group=getGroup(req); - if(!GroupStorage.getGroupMemberAdminLevel(group.id, self.user.id).isAtLeast(level)){ - throw new UserActionNotAllowedException(); - } + context(req).getGroupsController().enforceUserAdminLevel(group, self.user, level); return group; } @@ -130,11 +128,11 @@ public static Object doCreateGroup(Request req, Response resp, Account self){ } } - public static Object groupProfile(Request req, Response resp, Group group) throws SQLException{ + public static Object groupProfile(Request req, Response resp, Group group){ SessionInfo info=Utils.sessionInfo(req); @Nullable Account self=info!=null ? info.account : null; - List members=GroupStorage.getRandomMembersForProfile(group.id); + List members=context(req).getGroupsController().getRandomMembersForProfile(group); int offset=offset(req); PaginatedList wall=context(req).getWallController().getWallPosts(group, false, offset, 20); @@ -149,14 +147,14 @@ public static Object groupProfile(Request req, Response resp, Group group) throw model.with("group", group).with("members", members).with("postCount", wall.total).paginate(wall); model.with("postInteractions", interactions); model.with("title", group.name); - model.with("admins", GroupStorage.getGroupAdmins(group.id)); + model.with("admins", context(req).getGroupsController().getAdmins(group)); if(group instanceof ForeignGroup) model.with("noindex", true); jsLangKey(req, "yes", "no", "delete_post", "delete_post_confirm", "delete_reply", "delete_reply_confirm", "remove_friend", "cancel", "delete", "post_form_cw", "post_form_cw_placeholder", "attach_menu_photo", "attach_menu_cw", "attach_menu_poll", "max_file_size_exceeded", "max_attachment_count_exceeded", "remove_attachment"); jsLangKey(req, "create_poll_question", "create_poll_options", "create_poll_add_option", "create_poll_delete_option", "create_poll_multi_choice", "create_poll_anonymous", "create_poll_time_limit", "X_days", "X_hours"); if(self!=null){ - Group.AdminLevel level=GroupStorage.getGroupMemberAdminLevel(group.id, self.user.id); - model.with("membershipState", GroupStorage.getUserMembershipState(group.id, self.user.id)); + Group.AdminLevel level=context(req).getGroupsController().getMemberAdminLevel(group, self.user); + model.with("membershipState", context(req).getGroupsController().getUserMembershipState(group, self.user)); model.with("groupAdminLevel", level); if(level.isAtLeast(Group.AdminLevel.ADMIN)){ jsLangKey(req, "update_profile_picture", "save", "profile_pic_select_square_version", "drag_or_choose_file", "choose_file", @@ -232,7 +230,7 @@ public static Object leave(Request req, Response resp, Account self) throws SQLE return ""; } - public static Object editGeneral(Request req, Response resp, Account self) throws SQLException{ + public static Object editGeneral(Request req, Response resp, Account self){ Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.ADMIN); RenderedTemplateResponse model=new RenderedTemplateResponse("group_edit_general", req); model.with("group", group).with("title", group.name); @@ -283,32 +281,32 @@ public static Object members(Request req, Response resp){ return model; } - public static Object admins(Request req, Response resp) throws SQLException{ + public static Object admins(Request req, Response resp){ Group group=getGroup(req); RenderedTemplateResponse model=new RenderedTemplateResponse("group_admins", req); - model.with("admins", GroupStorage.getGroupAdmins(group.id)); + model.with("admins", context(req).getGroupsController().getAdmins(group)); if(isAjax(req)){ return new WebDeltaResponse(resp).box(lang(req).get("group_admins"), model.renderContentBlock(), null, true); } return model; } - public static Object editAdmins(Request req, Response resp, Account self) throws SQLException{ + public static Object editAdmins(Request req, Response resp, Account self){ Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.ADMIN); RenderedTemplateResponse model=new RenderedTemplateResponse("group_edit_admins", req); model.with("group", group).with("title", group.name); - model.with("admins", GroupStorage.getGroupAdmins(group.id)); + model.with("admins", context(req).getGroupsController().getAdmins(group)); jsLangKey(req, "cancel", "group_admin_demote", "yes", "no"); return model; } - public static Object editMembers(Request req, Response resp, Account self) throws SQLException{ + public static Object editMembers(Request req, Response resp, Account self){ Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.MODERATOR); - Group.AdminLevel level=GroupStorage.getGroupMemberAdminLevel(group.id, self.user.id); + Group.AdminLevel level=context(req).getGroupsController().getMemberAdminLevel(group, self.user); RenderedTemplateResponse model=new RenderedTemplateResponse("group_edit_members", req); model.paginate(context(req).getGroupsController().getMembers(group, offset(req), 100)); model.with("group", group).with("title", group.name); - model.with("adminIDs", GroupStorage.getGroupAdmins(group.id).stream().map(adm->adm.user.id).collect(Collectors.toList())); + model.with("adminIDs", context(req).getGroupsController().getAdmins(group).stream().map(adm->adm.user.id).collect(Collectors.toList())); model.with("canAddAdmins", level.isAtLeast(Group.AdminLevel.ADMIN)); model.with("adminLevel", level); jsLangKey(req, "cancel", "yes", "no"); @@ -356,23 +354,18 @@ public static Object saveAdmin(Request req, Response resp, Account self) throws return ""; } - public static Object confirmDemoteAdmin(Request req, Response resp, Account self) throws SQLException{ + public static Object confirmDemoteAdmin(Request req, Response resp, Account self){ Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.ADMIN); - int userID=parseIntOrDefault(req.queryParams("id"), 0); - User user=UserStorage.getById(userID); - if(user==null) - throw new ObjectNotFoundException("user_not_found"); - + int userID=safeParseInt(req.queryParams("id")); + User user=context(req).getUsersController().getUserOrThrow(userID); String back=Utils.back(req); return new RenderedTemplateResponse("generic_confirm", req).with("message", Utils.lang(req).get("group_admin_demote_confirm", Map.of("name", user.getFirstLastAndGender()))).with("formAction", Config.localURI("/groups/"+group.id+"/removeAdmin?_redir="+URLEncoder.encode(back)+"&id="+userID)).with("back", back); } public static Object removeAdmin(Request req, Response resp, Account self) throws SQLException{ Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.ADMIN); - int userID=parseIntOrDefault(req.queryParams("id"), 0); - User user=UserStorage.getById(userID); - if(user==null) - throw new ObjectNotFoundException("user_not_found"); + int userID=safeParseInt(req.queryParams("id")); + User user=context(req).getUsersController().getUserOrThrow(userID); GroupStorage.removeGroupAdmin(group.id, userID); @@ -407,7 +400,7 @@ public static Object blocking(Request req, Response resp, Account self) throws S return model; } - public static Object blockDomainForm(Request req, Response resp, Account self) throws SQLException{ + public static Object blockDomainForm(Request req, Response resp, Account self){ Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.MODERATOR); RenderedTemplateResponse model=new RenderedTemplateResponse("block_domain", req); return wrapForm(req, resp, "block_domain", "/groups/"+group.id+"/blockDomain", lang(req).get("block_a_domain"), "block", model); @@ -427,7 +420,7 @@ public static Object blockDomain(Request req, Response resp, Account self) throw return ""; } - public static Object confirmUnblockDomain(Request req, Response resp, Account self) throws SQLException{ + public static Object confirmUnblockDomain(Request req, Response resp, Account self){ Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.MODERATOR); String domain=req.queryParams("domain"); Lang l=Utils.lang(req); @@ -446,17 +439,12 @@ public static Object unblockDomain(Request req, Response resp, Account self) thr return ""; } - private static User getUserOrThrow(Request req) throws SQLException{ + private static User getUserOrThrow(Request req){ int id=parseIntOrDefault(req.queryParams("id"), 0); - if(id==0) - throw new ObjectNotFoundException("err_user_not_found"); - User user=UserStorage.getById(id); - if(user==null) - throw new ObjectNotFoundException("err_user_not_found"); - return user; + return context(req).getUsersController().getUserOrThrow(id); } - public static Object confirmBlockUser(Request req, Response resp, Account self) throws SQLException{ + public static Object confirmBlockUser(Request req, Response resp, Account self){ Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.MODERATOR); User user=getUserOrThrow(req); Lang l=Utils.lang(req); @@ -464,7 +452,7 @@ public static Object confirmBlockUser(Request req, Response resp, Account self) return new RenderedTemplateResponse("generic_confirm", req).with("message", l.get("confirm_block_user_X", Map.of("user", user.getFirstLastAndGender()))).with("formAction", "/groups/"+group.id+"/blockUser?id="+user.id+"&_redir="+URLEncoder.encode(back)).with("back", back); } - public static Object confirmUnblockUser(Request req, Response resp, Account self) throws SQLException{ + public static Object confirmUnblockUser(Request req, Response resp, Account self){ Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.MODERATOR); User user=getUserOrThrow(req); Lang l=Utils.lang(req); diff --git a/src/main/resources/templates/desktop/group.twig b/src/main/resources/templates/desktop/group.twig index 59182e09..60af45ec 100644 --- a/src/main/resources/templates/desktop/group.twig +++ b/src/main/resources/templates/desktop/group.twig @@ -23,7 +23,7 @@ {% endif %}
    - {% include 'wall_profile_block' with {'wallOwner': group, 'isGroup': true, 'fullWallURL': "/groups/#{group.id}/wall"} %} + {% include "profile_module_wall" with {'wallOwner': user, 'isGroup': true, 'headerTitle': L('wall'), 'headerHref': "/groups/#{group.id}/wall", 'additionalHeader': L('X_posts', {'count': totalItems})} %}
    - - - {% for member in members %} - {% if(loop.first or loop.index==3) %}{% endif %} - - {%if(loop.index==2 or loop.index==5)%}{%endif%} - {%endfor%} - {%if(members.size%3!=0)%} - - {%endif%} -
    {{ L("members") }}
    {{ L('X_members', {'count': group.memberCount}) }}
    - - {{member | pictureForAvatar('s')}}
    - {{member.firstName}}
    {{member.lastName}}
    -
    -
     
    - - - - - - {% for admin in admins %} - {% if loop.index<5 %} - - - - - {% endif %} - {% endfor %} -
    {{ L("group_admins") }}
    {{ L('group_X_admins', {'count': admins.size}) }}
    {{ admin.user | pictureForAvatar('s', 40) }} - - {% if admin.title is not empty %} -
    {{ admin.title }}
    - {% endif %} -
    - + {% include "profile_module_user_grid" with {'users': members, 'headerTitle': L('members'), 'headerHref': "/groups/#{group.id}/members", 'subheaderTitle': L('X_members', {'count': group.memberCount})} %} + {% include "profile_module_user_list" with {'users': admins, 'headerTitle': L('group_admins'), 'headerHref': "/groups/#{group.id}/admins", 'headerAjaxBox': true, 'subheaderTitle': L('group_X_admins', {'count': admins.size})} %}
    diff --git a/src/main/resources/templates/desktop/page.twig b/src/main/resources/templates/desktop/page.twig index d3b3dd1b..70ed34a4 100644 --- a/src/main/resources/templates/desktop/page.twig +++ b/src/main/resources/templates/desktop/page.twig @@ -1,7 +1,6 @@ - {{ title }} {% if activityPubURL is not null %} diff --git a/src/main/resources/templates/desktop/profile.twig b/src/main/resources/templates/desktop/profile.twig index 5391f1c1..c45c4b30 100644 --- a/src/main/resources/templates/desktop/profile.twig +++ b/src/main/resources/templates/desktop/profile.twig @@ -71,53 +71,13 @@
    {% if mutualFriends is not empty %} - - - - {%for friend in mutualFriends%} - {%if(loop.first or loop.index==3)%}{%endif%} - - {%if(loop.index==2 or loop.index==5)%}{%endif%} - {%endfor%} - {%if(friends.size%3!=0)%} - - {%endif%} -
    {{L("mutual_friends")}}
    {{L('X_mutual_friends', {'count': mutualFriendCount})}}
    - - {{friend | pictureForAvatar('s')}}
    - {{friend.firstName}} -
    -
     
    + {% include "profile_module_user_grid" with {'users': mutualFriends, 'headerTitle': L('mutual_friends'), 'headerHref': "/users/#{user.id}/friends/mutual", 'subheaderTitle': L('X_mutual_friends', {'count': mutualFriendCount})} %} + {% endif %} + {% if friends is not empty %} + {% include "profile_module_user_grid" with {'users': friends, 'headerTitle': L('friends'), 'headerHref': "/users/#{user.id}/friends", 'subheaderTitle': L('X_friends', {'count': friendCount})} %} {% endif %} - - - - - {%for friend in friends%} - {%if(loop.first or loop.index==3)%}{%endif%} - - {%if(loop.index==2 or loop.index==5)%}{%endif%} - {%endfor%} - {%if(friends.size%3!=0)%} - - {%endif%} -
    {{L("friends")}}
    {{L('X_friends', {'count': friendCount})}}
    - - {{friend | pictureForAvatar('s')}}
    - {{friend.firstName}}
    {{friend.lastName}}
    -
    -
     
    - {% if groups is not empty %} - - - - -
    {{ L('groups') }}
    {{ L('X_groups', {'count': groups.size}) }}
    - {% for group in groups %} - {{ group.name }}{% if not loop.last %} • {% endif %} - {% endfor %} -
    + {% include "profile_module_groups" with {'groups': groups, 'headerTitle': L('groups'), 'headerHref': "/users/#{user.id}/groups", 'subheaderTitle': L('X_groups', {'count': groups.size})} %} {% endif %}
    @@ -134,7 +94,7 @@ {% endfor %}
    - {% include "wall_profile_block" with {'wallOwner': user, 'fullWallURL': "/users/#{user.id}/wall"} %} + {% include "profile_module_wall" with {'wallOwner': user, 'headerTitle': L('wall'), 'headerHref': "/users/#{user.id}/wall", 'additionalHeader': L('X_posts', {'count': totalItems})} %} diff --git a/src/main/resources/templates/desktop/profile_module.twig b/src/main/resources/templates/desktop/profile_module.twig new file mode 100644 index 00000000..938ce50c --- /dev/null +++ b/src/main/resources/templates/desktop/profile_module.twig @@ -0,0 +1,17 @@ +
    +
    + {{ headerTitle }} + {%- if additionalHeader is not empty %} + {%- if additionalHeaderHref is not empty %} + {{ additionalHeader }} + {%- else %} + {{ additionalHeader }} + {%- endif -%} + {%- endif -%} +
    + {% if subheaderTitle is not empty %} +
    {{ subheaderTitle }}
    + {% endif %} + {% block moduleContent %} + {% endblock %} +
    diff --git a/src/main/resources/templates/desktop/profile_module_groups.twig b/src/main/resources/templates/desktop/profile_module_groups.twig new file mode 100644 index 00000000..d2c8db86 --- /dev/null +++ b/src/main/resources/templates/desktop/profile_module_groups.twig @@ -0,0 +1,8 @@ +{% extends "profile_module" %} +{% block moduleContent %} +
    +{% for group in groups %} + {{ group.name }}{% if not loop.last %} • {% endif %} +{% endfor %} +
    +{% endblock %} \ No newline at end of file diff --git a/src/main/resources/templates/desktop/profile_module_user_grid.twig b/src/main/resources/templates/desktop/profile_module_user_grid.twig new file mode 100644 index 00000000..88d3a6a3 --- /dev/null +++ b/src/main/resources/templates/desktop/profile_module_user_grid.twig @@ -0,0 +1,20 @@ +{# @pebvariable name="users" type="smithereen.data.User[]" #} +{% extends "profile_module" %} +{% block moduleContent %} + + {% for friend in users %} + {% if loop.first or loop.index==3 %}{% endif %} + + {%if loop.index==2 or loop.index==5 %}{% endif %} + {% endfor %} + {% if users.size%3!=0 %} + + {% endif %} +
    + + {{ friend | pictureForAvatar('s') }}
    + {{ friend.firstName }}
    {{ friend.lastName }}
    +
    +
     
    +{% endblock %} + diff --git a/src/main/resources/templates/desktop/profile_module_user_list.twig b/src/main/resources/templates/desktop/profile_module_user_list.twig new file mode 100644 index 00000000..d3b52a91 --- /dev/null +++ b/src/main/resources/templates/desktop/profile_module_user_list.twig @@ -0,0 +1,19 @@ +{% extends "profile_module" %} +{% block moduleContent %} + + +{% for admin in users %} + {% if loop.index<5 %} + + + + + {% endif %} +{% endfor %} +
    {{ admin.user | pictureForAvatar('s', 40) }} + + {% if admin.title is not empty %} +
    {{ admin.title }}
    + {% endif %} +
    +{% endblock %} \ No newline at end of file diff --git a/src/main/resources/templates/desktop/profile_module_wall.twig b/src/main/resources/templates/desktop/profile_module_wall.twig new file mode 100644 index 00000000..5dbb9f68 --- /dev/null +++ b/src/main/resources/templates/desktop/profile_module_wall.twig @@ -0,0 +1,14 @@ +{% extends "profile_module" %} +{% block moduleContent %} + {% if currentUser is not null and not isSelfBlocked %} + {% include "wall_post_form" with {} %} + {%endif%} +
    + {%for post in items%} +
    {%include "wall_post" with {'post': post}%}
    + {% else %} +
    {{ L('wall_empty') }}
    + {%endfor%} +
    +
    {% include "pagination" %}
    +{% endblock %} diff --git a/src/main/resources/templates/desktop/wall_profile_block.twig b/src/main/resources/templates/desktop/wall_profile_block.twig deleted file mode 100644 index fe46703b..00000000 --- a/src/main/resources/templates/desktop/wall_profile_block.twig +++ /dev/null @@ -1,21 +0,0 @@ - - - - {% if currentUser is not null and not isSelfBlocked %} - - {%endif%} - - -
    {{L('wall')}}
    {{L('X_posts', {'count': postCount})}}
    -
    {% include "wall_post_form" %}
    -
    - {%for post in items%} -
    {%include "wall_post" with {'post': post}%}
    - {% else %} -
    {{ L('wall_empty') }}
    - {%endfor%} -
    -
    - {% include "pagination" %} -
    -
    diff --git a/src/main/resources/templates/mobile/page.twig b/src/main/resources/templates/mobile/page.twig index a01d5703..8ae4df60 100644 --- a/src/main/resources/templates/mobile/page.twig +++ b/src/main/resources/templates/mobile/page.twig @@ -1,7 +1,6 @@ - {{ title }} {% if activityPubURL is not null %} diff --git a/src/main/web/common_ts/Helpers.ts b/src/main/web/common_ts/Helpers.ts index ec85d15d..8fcbdd8d 100644 --- a/src/main/web/common_ts/Helpers.ts +++ b/src/main/web/common_ts/Helpers.ts @@ -440,11 +440,11 @@ function ajaxSubmitForm(form:HTMLFormElement, onDone:{(resp?:any):void}=null):bo } function ajaxFollowLink(link:HTMLAnchorElement):boolean{ - if(link.dataset.ajax){ + if(link.dataset.ajax!=undefined){ ajaxGetAndApplyActions(link.href); return true; } - if(link.dataset.ajaxBox){ + if(link.dataset.ajaxBox!=undefined){ LayerManager.getInstance().showBoxLoader(); ajaxGetAndApplyActions(link.href); return true; diff --git a/src/main/web/desktop.scss b/src/main/web/desktop.scss index fedd0391..907e8480 100644 --- a/src/main/web/desktop.scss +++ b/src/main/web/desktop.scss @@ -540,22 +540,28 @@ small{ .profileBlock{ border-collapse: collapse; + overflow-x: hidden; .blockHeader{ text-align: left; height: 20px; border-top: solid 1px $blockBorderTop; - padding: 0 8px; + padding: 3px 8px; background: $blockBackground; + font-weight: bold; a{ color: $boldHeaders; } + .addHeader{ + color: $auxiliaryBlockHeader; + text-transform: lowercase; + } } .blockSubheader{ height: 19px; background: $wallCommentSeparator; border-top: solid 1px $blockBorderBottom; color: $auxiliaryGrey; - padding: 0 8px; + padding: 3px 8px; } } @@ -1942,3 +1948,7 @@ label.pretendLink{ } } } + +.fixLineHeight{ + line-height: 13px; +} From 278733695883e8dcffb449c4d6a2887ff53b5970 Mon Sep 17 00:00:00 2001 From: Grishka Date: Sun, 21 Nov 2021 21:26:11 +0300 Subject: [PATCH 08/65] Allow setting additional properties via env vars --- src/main/java/smithereen/Config.java | 2 +- src/main/java/smithereen/SmithereenApplication.java | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/smithereen/Config.java b/src/main/java/smithereen/Config.java index b9de2d35..3709e018 100644 --- a/src/main/java/smithereen/Config.java +++ b/src/main/java/smithereen/Config.java @@ -50,7 +50,7 @@ public class Config{ public static long mediaCacheFileSizeLimit; public static boolean useHTTP; public static String staticFilesPath; - public static final boolean DEBUG=System.getProperty("smithereen.debug")!=null; + public static final boolean DEBUG=System.getProperty("smithereen.debug")!=null || System.getenv("SMITHEREEN_DEBUG")!=null; public static String imgproxyLocalUploads; public static String imgproxyLocalMediaCache; diff --git a/src/main/java/smithereen/SmithereenApplication.java b/src/main/java/smithereen/SmithereenApplication.java index 19cb0479..b0628d75 100644 --- a/src/main/java/smithereen/SmithereenApplication.java +++ b/src/main/java/smithereen/SmithereenApplication.java @@ -9,6 +9,7 @@ import java.io.StringWriter; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.sql.Timestamp; @@ -74,6 +75,13 @@ public class SmithereenApplication{ System.setProperty("org.slf4j.simpleLogger.showShortLogName", "true"); if(Config.DEBUG) System.setProperty("org.slf4j.simpleLogger.log.smithereen", "trace"); + String addProperties=System.getenv("SMITHEREEN_SET_PROPS"); + if(addProperties!=null){ + Arrays.stream(addProperties.split("&")).forEach(s->{ + String[] kv=s.split("=", 2); + System.setProperty(kv[0], URLDecoder.decode(kv[1], StandardCharsets.UTF_8)); + }); + } LOG=LoggerFactory.getLogger(SmithereenApplication.class); context=new ApplicationContext(); From c346d26fff6d59fba6f4ce5cd400f5bdb2d839c0 Mon Sep 17 00:00:00 2001 From: Grishka Date: Mon, 22 Nov 2021 21:57:42 +0300 Subject: [PATCH 09/65] Sanity check post urls (Lemmy misuses them) --- src/main/java/smithereen/data/Post.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/smithereen/data/Post.java b/src/main/java/smithereen/data/Post.java index 11901681..c7aff5ab 100644 --- a/src/main/java/smithereen/data/Post.java +++ b/src/main/java/smithereen/data/Post.java @@ -240,6 +240,8 @@ public JsonObject asActivityPubObject(JsonObject obj, ContextCollector contextCo @Override protected ActivityPubObject parseActivityPubObject(JsonObject obj, ParserContext parserContext){ super.parseActivityPubObject(obj, parserContext); + // fix for Lemmy (and possibly something else) + boolean hasBogusURL=url!=null && !url.getHost().equalsIgnoreCase(activityPubID.getHost()); JsonElement _content=obj.get("content"); if(_content!=null && _content.isJsonArray()){ content=_content.getAsJsonArray().get(0).getAsString(); @@ -249,6 +251,8 @@ protected ActivityPubObject parseActivityPubObject(JsonObject obj, ParserContext if(content!=null && !parserContext.isLocal){ if(StringUtils.isNotEmpty(name) && !isPoll) content="

    "+name+"

    "+content; + if(hasBogusURL) + content=content+"

    "+url+"

    "; content=Utils.sanitizeHTML(content); if(obj.has("sensitive") && obj.get("sensitive").getAsBoolean() && summary!=null){ summary=Utils.sanitizeHTML(summary); @@ -256,6 +260,8 @@ protected ActivityPubObject parseActivityPubObject(JsonObject obj, ParserContext summary=null; } } + if(hasBogusURL) + url=activityPubID; try{ user=UserStorage.getUserByActivityPubID(attributedTo); if(url==null) From d9acc38ccfb49700893e595dc28d84b3198b083b Mon Sep 17 00:00:00 2001 From: Grishka Date: Mon, 13 Dec 2021 12:13:49 +0300 Subject: [PATCH 10/65] fix libvips on ARM --- src/main/java/smithereen/libvips/LibVips.java | 46 +++++++++++++++---- .../java/smithereen/libvips/VipsImage.java | 16 +++---- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/main/java/smithereen/libvips/LibVips.java b/src/main/java/smithereen/libvips/LibVips.java index 295e129f..c415049e 100644 --- a/src/main/java/smithereen/libvips/LibVips.java +++ b/src/main/java/smithereen/libvips/LibVips.java @@ -22,31 +22,51 @@ class LibVips{ vips_init(""); if(Config.DEBUG) vips_leak_set(true); + + varArgsWrapper=Native.load(LibVipsVarArgsWrapper.class); } + private static LibVipsVarArgsWrapper varArgsWrapper; + static native int vips_init(String argv0); static native String vips_foreign_find_load(String filename); static native void vips_leak_set(boolean leak); - static native Pointer vips_image_new_from_file(String name, Pointer _null); + static Pointer vips_image_new_from_file(String name){ + return varArgsWrapper.vips_image_new_from_file(name, Pointer.NULL); + } static native String vips_error_buffer(); static native void vips_error_clear(); static native int vips_image_get_width(Pointer img); static native int vips_image_get_height(Pointer img); - static native int vips_resize(Pointer in, PointerByReference out, double scale, Pointer _null); - static native int vips_resize(Pointer in, PointerByReference out, double scale, String _vscale, double vscale, Pointer _null); - static native int vips_image_write_to_file(Pointer img, String fileName, Pointer _null); + static int vips_resize(Pointer in, PointerByReference out, double scale){ + return varArgsWrapper.vips_resize(in, out, scale, Pointer.NULL); + } + static int vips_resize(Pointer in, PointerByReference out, double scale, double vscale){ + return varArgsWrapper.vips_resize(in, out, scale, "vscale", vscale, Pointer.NULL); + } + static int vips_image_write_to_file(Pointer img, String fileName){ + return varArgsWrapper.vips_image_write_to_file(img, fileName, Pointer.NULL); + } static native boolean vips_image_hasalpha(Pointer img); static native Pointer vips_array_double_new(double[] array, int n); static native void vips_area_unref(Pointer area); - static native int vips_flatten(Pointer in, PointerByReference out, String bgName, Pointer bgValue, Pointer _null); - static native int vips_crop(Pointer in, PointerByReference out, int left, int top, int width, int height, Pointer _null); + static int vips_flatten(Pointer in, PointerByReference out, Pointer bgValue){ + return varArgsWrapper.vips_flatten(in, out, "background", bgValue, Pointer.NULL); + } + static int vips_crop(Pointer in, PointerByReference out, int left, int top, int width, int height){ + return varArgsWrapper.vips_crop(in, out, left, top, width, height, Pointer.NULL); + } static native int vips_image_get_bands(Pointer img); static native int vips_image_get_format(Pointer img); - static native int vips_cast_uchar(Pointer img, PointerByReference out, Pointer _null); + static int vips_cast_uchar(Pointer img, PointerByReference out){ + return varArgsWrapper.vips_cast_uchar(img, out, Pointer.NULL); + } static native Pointer vips_image_get_fields(Pointer img); static native Pointer vips_image_get_typeof(Pointer img, String name); static native boolean vips_image_remove(Pointer img, String name); - static native int vips_icc_transform(Pointer img, PointerByReference out, String outputProfile, Pointer _null); + static int vips_icc_transform(Pointer img, PointerByReference out, String outputProfile){ + return varArgsWrapper.vips_icc_transform(img, out, outputProfile, Pointer.NULL); + } static native Pointer vips_region_new(Pointer img); static native int vips_region_prepare(Pointer region, VipsRect rect); @@ -66,5 +86,15 @@ static class LibGLib{ static native void g_free(Pointer ptr); static native void g_strfreev(Pointer ptr); } + + private interface LibVipsVarArgsWrapper extends Library{ + Pointer vips_image_new_from_file(String name, Object... args); + int vips_resize(Pointer in, PointerByReference out, double scale, Object... args); + int vips_flatten(Pointer in, PointerByReference out, Object... args); + int vips_crop(Pointer in, PointerByReference out, int left, int top, int width, int height, Object... args); + int vips_image_write_to_file(Pointer img, String fileName, Object... args); + int vips_cast_uchar(Pointer img, PointerByReference out, Object... args); + int vips_icc_transform(Pointer img, PointerByReference out, String outputProfile, Object... args); + } } diff --git a/src/main/java/smithereen/libvips/VipsImage.java b/src/main/java/smithereen/libvips/VipsImage.java index 4955c148..4c553d37 100644 --- a/src/main/java/smithereen/libvips/VipsImage.java +++ b/src/main/java/smithereen/libvips/VipsImage.java @@ -37,7 +37,7 @@ public VipsImage(String filePath) throws IOException{ if(loader.equals("VipsForeignLoadJpegFile")){ filePath+="[autorotate=true]"; } - nativePtr=vips_image_new_from_file(filePath, Pointer.NULL); + nativePtr=vips_image_new_from_file(filePath); if(nativePtr==Pointer.NULL){ throwError(); } @@ -66,7 +66,7 @@ public void release(){ public VipsImage resize(double scale) throws IOException{ ensureNotReleased(); PointerByReference out=new PointerByReference(); - if(vips_resize(nativePtr, out, scale, Pointer.NULL)!=0){ + if(vips_resize(nativePtr, out, scale)!=0){ throwError(); } return new VipsImage(out.getValue()); @@ -75,7 +75,7 @@ public VipsImage resize(double scale) throws IOException{ public VipsImage resize(double hscale, double vscale) throws IOException{ ensureNotReleased(); PointerByReference out=new PointerByReference(); - if(vips_resize(nativePtr, out, hscale, "vscale", vscale, Pointer.NULL)!=0){ + if(vips_resize(nativePtr, out, hscale, vscale)!=0){ throwError(); } return new VipsImage(out.getValue()); @@ -84,7 +84,7 @@ public VipsImage resize(double hscale, double vscale) throws IOException{ public VipsImage crop(int left, int top, int width, int height) throws IOException{ ensureNotReleased(); PointerByReference out=new PointerByReference(); - if(vips_crop(nativePtr, out, left, top, width, height, Pointer.NULL)!=0){ + if(vips_crop(nativePtr, out, left, top, width, height)!=0){ throwError(); } return new VipsImage(out.getValue()); @@ -92,7 +92,7 @@ public VipsImage crop(int left, int top, int width, int height) throws IOExcepti public void writeToFile(String fileName) throws IOException{ ensureNotReleased(); - if(vips_image_write_to_file(nativePtr, fileName, Pointer.NULL)!=0){ + if(vips_image_write_to_file(nativePtr, fileName)!=0){ throwError(); } } @@ -107,7 +107,7 @@ public VipsImage flatten(double r, double g, double b) throws IOException{ Pointer arr=vips_array_double_new(new double[]{r, g, b}, 3); PointerByReference out=new PointerByReference(); try{ - if(vips_flatten(nativePtr, out, "background", arr, Pointer.NULL)!=0){ + if(vips_flatten(nativePtr, out, arr)!=0){ throwError(); } }finally{ @@ -129,7 +129,7 @@ public BandFormat getFormat(){ public VipsImage castUChar() throws IOException{ ensureNotReleased(); PointerByReference out=new PointerByReference(); - if(vips_cast_uchar(nativePtr, out, Pointer.NULL)!=0) + if(vips_cast_uchar(nativePtr, out)!=0) throwError(); return new VipsImage(out.getValue()); } @@ -159,7 +159,7 @@ public boolean hasField(String name){ public VipsImage iccTransform(String outputProfile) throws IOException{ ensureNotReleased(); PointerByReference out=new PointerByReference(); - if(vips_icc_transform(nativePtr, out, outputProfile, Pointer.NULL)!=0) + if(vips_icc_transform(nativePtr, out, outputProfile)!=0) throwError(); return new VipsImage(out.getValue()); } From 61c5b6500d3ec6bbf992527c54387597c52ba76d Mon Sep 17 00:00:00 2001 From: Grishka Date: Wed, 15 Dec 2021 11:00:41 +0300 Subject: [PATCH 11/65] Events --- Dockerfile | 2 +- README.md | 4 +- pom.xml | 5 +- .../smithereen/SmithereenApplication.java | 13 +- src/main/java/smithereen/Utils.java | 10 ++ .../objects/ActivityPubObject.java | 1 + .../smithereen/activitypub/objects/Event.java | 8 ++ .../controllers/GroupsController.java | 29 ++++- src/main/java/smithereen/data/Account.java | 9 +- .../java/smithereen/data/ForeignGroup.java | 11 ++ src/main/java/smithereen/data/Group.java | 19 +++ .../java/smithereen/jsonld/JLDProcessor.java | 35 ++--- src/main/java/smithereen/lang/Lang.java | 8 +- .../java/smithereen/routes/GroupsRoutes.java | 121 +++++++++++++----- .../routes/SettingsAdminRoutes.java | 5 +- .../java/smithereen/storage/GroupStorage.java | 47 +++++-- .../java/smithereen/storage/PostStorage.java | 4 +- .../smithereen/storage/SessionStorage.java | 3 +- .../templates/InstantToDateFunction.java | 28 ++++ .../templates/InstantToTimeFunction.java | 29 +++++ .../templates/SmithereenExtension.java | 4 +- .../java/smithereen/templates/Templates.java | 18 +++ src/main/resources/langs/en.json | 13 +- src/main/resources/langs/ru.json | 13 +- .../templates/common/admin_users.twig | 8 +- .../templates/common/create_event.twig | 6 + .../templates/common/events_tabbar.twig | 5 + .../templates/common/group_admin_tabbar.twig | 2 +- .../templates/common/group_edit_general.twig | 6 +- .../resources/templates/desktop/forms.twig | 20 +++ .../resources/templates/desktop/group.twig | 19 +-- .../resources/templates/desktop/groups.twig | 8 +- .../templates/desktop/wall_post_form.twig | 2 +- .../resources/templates/mobile/forms.twig | 19 +++ .../resources/templates/mobile/group.twig | 14 +- .../resources/templates/mobile/groups.twig | 4 +- .../templates/mobile/wall_post_form.twig | 2 +- src/main/web/common.scss | 28 +++- src/main/web/desktop.scss | 25 +++- src/main/web/img/calendar_input.svg | 10 ++ src/main/web/img/time_input.svg | 8 ++ src/main/web/mobile.scss | 17 ++- src/main/web/package-lock.json | 41 +++--- src/main/web/package.json | 6 +- .../java/smithereen/HTMLSanitizerTest.java | 4 +- 45 files changed, 545 insertions(+), 148 deletions(-) create mode 100644 src/main/java/smithereen/activitypub/objects/Event.java create mode 100644 src/main/java/smithereen/templates/InstantToDateFunction.java create mode 100644 src/main/java/smithereen/templates/InstantToTimeFunction.java create mode 100644 src/main/resources/templates/common/create_event.twig create mode 100644 src/main/resources/templates/common/events_tabbar.twig create mode 100644 src/main/web/img/calendar_input.svg create mode 100644 src/main/web/img/time_input.svg diff --git a/Dockerfile b/Dockerfile index 106a0c55..3b514165 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM maven:3.8.3-eclipse-temurin-17 as builder WORKDIR /usr/src/app COPY . . -RUN mvn package +RUN mvn package -DskipTests=true RUN java LibVipsDownloader.java FROM eclipse-temurin:17-jdk diff --git a/README.md b/README.md index 226e0f34..3ce7bd01 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ If you have any questions or feedback, there's a [Telegram chat](https://t.me/Sm ### Running directly on your server 1. Install and configure MySQL -2. Install maven and JDK >=15 if you don't have it already -3. Build the jar by running `mvn package` and place the one with dependencies at `/opt/smithereen/smithereen.jar` +2. Install maven and JDK >=17 if you don't have it already +3. Build the jar by running `mvn package -DskipTests=true` and place the one with dependencies at `/opt/smithereen/smithereen.jar` 4. Set up the image processing native library ([libvips](https://github.com/libvips/libvips)): run `java LibVipsDownloader.java` to automatically download a prebuilt one from [here](https://github.com/lovell/sharp-libvips). If you already have libvips installed on your system, you may skip this step, but be aware that not all libvips builds include all the features Smithereen needs. 5. Install and configure [imgproxy](https://docs.imgproxy.net/#/GETTING_STARTED) 6. Fill in the config file, see a commented example [here](examples/config.properties) diff --git a/pom.xml b/pom.xml index 12fcbeaf..8da3379a 100644 --- a/pom.xml +++ b/pom.xml @@ -8,6 +8,8 @@ UTF-8 17 17 + true + me.grishka.smithereen @@ -54,9 +56,6 @@ maven-surefire-plugin 3.0.0-M4 - - true - diff --git a/src/main/java/smithereen/SmithereenApplication.java b/src/main/java/smithereen/SmithereenApplication.java index b0628d75..9b3b4d91 100644 --- a/src/main/java/smithereen/SmithereenApplication.java +++ b/src/main/java/smithereen/SmithereenApplication.java @@ -13,6 +13,7 @@ import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.sql.Timestamp; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.Objects; @@ -143,10 +144,9 @@ public static void main(String[] args){ info.account=UserStorage.getAccount(info.account.id); info.permissions=SessionStorage.getUserPermissions(info.account); - if(System.currentTimeMillis()-info.account.lastActive.getTime()>=10*60*1000){ - Timestamp now=new Timestamp(System.currentTimeMillis()); - info.account.lastActive=now; - SessionStorage.setLastActive(info.account.id, request.cookie("psid"), now); + if(System.currentTimeMillis()-info.account.lastActive.toEpochMilli()>=10*60*1000){ + info.account.lastActive=Instant.now(); + SessionStorage.setLastActive(info.account.id, request.cookie("psid"), info.account.lastActive); } } // String hs=""; @@ -393,6 +393,11 @@ public static void main(String[] args){ getLoggedIn("/create", GroupsRoutes::createGroup); postWithCSRF("/create", GroupsRoutes::doCreateGroup); }); + path("/events", ()->{ + getLoggedIn("", GroupsRoutes::myEvents); + getLoggedIn("/past", GroupsRoutes::myPastEvents); + getLoggedIn("/create", GroupsRoutes::createEvent); + }); }); path("/api/v1", ()->{ diff --git a/src/main/java/smithereen/Utils.java b/src/main/java/smithereen/Utils.java index 6c736045..96cdd448 100644 --- a/src/main/java/smithereen/Utils.java +++ b/src/main/java/smithereen/Utils.java @@ -28,6 +28,9 @@ import java.sql.SQLException; import java.text.SimpleDateFormat; import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.time.temporal.ChronoField; @@ -770,6 +773,13 @@ public static ApplicationContext context(Request req){ return context; } + @NotNull + public static Instant instantFromDateAndTime(Request req, String dateStr, String timeStr){ + LocalDate date=DateTimeFormatter.ISO_LOCAL_DATE.parse(dateStr, LocalDate::from); + LocalTime time=DateTimeFormatter.ISO_LOCAL_TIME.parse(timeStr, LocalTime::from); + return LocalDateTime.of(date, time).atZone(timeZoneForRequest(req).toZoneId()).toInstant(); + } + public interface MentionCallback{ User resolveMention(String username, String domain); User resolveMention(String uri); diff --git a/src/main/java/smithereen/activitypub/objects/ActivityPubObject.java b/src/main/java/smithereen/activitypub/objects/ActivityPubObject.java index 52d28f7f..675f7990 100644 --- a/src/main/java/smithereen/activitypub/objects/ActivityPubObject.java +++ b/src/main/java/smithereen/activitypub/objects/ActivityPubObject.java @@ -551,6 +551,7 @@ public static ActivityPubObject parse(JsonObject obj, ParserContext parserContex case "Mention" -> new Mention(); case "Relationship" -> new Relationship(); case "PropertyValue" -> new PropertyValue(); + case "Event" -> new Event(); // Collections case "Collection" -> new ActivityPubCollection(false); diff --git a/src/main/java/smithereen/activitypub/objects/Event.java b/src/main/java/smithereen/activitypub/objects/Event.java new file mode 100644 index 00000000..88d14b9f --- /dev/null +++ b/src/main/java/smithereen/activitypub/objects/Event.java @@ -0,0 +1,8 @@ +package smithereen.activitypub.objects; + +public class Event extends ActivityPubObject{ + @Override + public String getType(){ + return "Event"; + } +} diff --git a/src/main/java/smithereen/controllers/GroupsController.java b/src/main/java/smithereen/controllers/GroupsController.java index 48940a2d..b498c90f 100644 --- a/src/main/java/smithereen/controllers/GroupsController.java +++ b/src/main/java/smithereen/controllers/GroupsController.java @@ -46,7 +46,9 @@ private Group createGroupInternal(User admin, String name, String description, b try{ if(StringUtils.isEmpty(name)) throw new IllegalArgumentException("name is empty"); - int id=GroupStorage.createGroup(name, Utils.preprocessPostHTML(description, null), description, admin.id, false); + if(isEvent && startTime==null) + throw new IllegalArgumentException("start time is required for event"); + int id=GroupStorage.createGroup(name, Utils.preprocessPostHTML(description, null), description, admin.id, isEvent, startTime, endTime); Group group=Objects.requireNonNull(GroupStorage.getById(id)); ActivityPubWorker.getInstance().sendAddToGroupsCollectionActivity(admin, group); return group; @@ -71,6 +73,14 @@ public PaginatedList getUserManagedGroups(@NotNull User user, int offset, } } + public PaginatedList getUserEvents(@NotNull User user, EventsType type, int offset, int count){ + try{ + return GroupStorage.getUserEvents(user.id, type, offset, count); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + public Group getGroupOrThrow(int id){ try{ if(id<=0) @@ -137,4 +147,21 @@ public void enforceUserAdminLevel(@NotNull Group group, @NotNull User user, @Not if(!getMemberAdminLevel(group, user).isAtLeast(atLeastLevel)) throw new UserActionNotAllowedException(); } + + public void updateGroupInfo(@NotNull Group group, @NotNull User admin, String name, String aboutSrc, Instant eventStart, Instant eventEnd){ + try{ + enforceUserAdminLevel(group, admin, Group.AdminLevel.ADMIN); + String about=Utils.preprocessPostHTML(aboutSrc, null); + GroupStorage.updateGroupGeneralInfo(group, name, aboutSrc, about, eventStart, eventEnd); + ActivityPubWorker.getInstance().sendUpdateGroupActivity(group); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public enum EventsType{ + FUTURE, + PAST, + ALL + } } diff --git a/src/main/java/smithereen/data/Account.java b/src/main/java/smithereen/data/Account.java index 4d171cc3..076ef30f 100644 --- a/src/main/java/smithereen/data/Account.java +++ b/src/main/java/smithereen/data/Account.java @@ -8,6 +8,7 @@ import java.time.Instant; import smithereen.Utils; +import smithereen.storage.DatabaseUtils; import smithereen.storage.UserStorage; public class Account{ @@ -16,8 +17,8 @@ public class Account{ public User user; public AccessLevel accessLevel; public UserPreferences prefs; - public Timestamp createdAt; - public Timestamp lastActive; + public Instant createdAt; + public Instant lastActive; public BanInfo banInfo; public User invitedBy; // used in admin UIs @@ -42,8 +43,8 @@ public static Account fromResultSet(ResultSet res) throws SQLException{ acc.email=res.getString("email"); acc.accessLevel=AccessLevel.values()[res.getInt("access_level")]; acc.user=UserStorage.getById(res.getInt("user_id")); - acc.createdAt=res.getTimestamp("created_at"); - acc.lastActive=res.getTimestamp("last_active"); + acc.createdAt=DatabaseUtils.getInstant(res, "created_at"); + acc.lastActive=DatabaseUtils.getInstant(res, "last_active"); String ban=res.getString("ban_info"); if(ban!=null) acc.banInfo=Utils.gson.fromJson(ban, BanInfo.class); diff --git a/src/main/java/smithereen/data/ForeignGroup.java b/src/main/java/smithereen/data/ForeignGroup.java index 38456294..1454ab51 100644 --- a/src/main/java/smithereen/data/ForeignGroup.java +++ b/src/main/java/smithereen/data/ForeignGroup.java @@ -14,6 +14,7 @@ import smithereen.Utils; import smithereen.activitypub.ParserContext; import smithereen.activitypub.objects.ActivityPubObject; +import smithereen.activitypub.objects.Event; import smithereen.activitypub.objects.ForeignActor; import smithereen.exceptions.BadRequestException; import spark.utils.StringUtils; @@ -61,6 +62,16 @@ protected ActivityPubObject parseActivityPubObject(JsonObject obj, ParserContext } wall=tryParseURL(optString(obj, "wall")); + if(attachment!=null && !attachment.isEmpty()){ + for(ActivityPubObject att:attachment){ + if(att instanceof Event ev){ + type=Type.EVENT; + eventStartTime=ev.startTime; + eventEndTime=ev.endTime; + } + } + } + return this; } diff --git a/src/main/java/smithereen/data/Group.java b/src/main/java/smithereen/data/Group.java index d9e2b869..9c43bdef 100644 --- a/src/main/java/smithereen/data/Group.java +++ b/src/main/java/smithereen/data/Group.java @@ -6,18 +6,23 @@ import java.net.URI; import java.sql.ResultSet; import java.sql.SQLException; +import java.time.Instant; import java.util.List; import smithereen.Config; import smithereen.activitypub.ContextCollector; import smithereen.activitypub.objects.Actor; +import smithereen.activitypub.objects.Event; import smithereen.jsonld.JLD; +import smithereen.storage.DatabaseUtils; import spark.utils.StringUtils; public class Group extends Actor{ public int id; public int memberCount; + public Type type=Type.GROUP; + public Instant eventStartTime, eventEndTime; public List adminsForActivityPub; @@ -65,6 +70,16 @@ protected void fillFromResultSet(ResultSet res) throws SQLException{ url=Config.localURI(username); memberCount=res.getInt("member_count"); summary=res.getString("about"); + eventStartTime=DatabaseUtils.getInstant(res, "event_start_time"); + eventEndTime=DatabaseUtils.getInstant(res, "event_end_time"); + type=Type.values()[res.getInt("type")]; + + if(type==Type.EVENT){ + Event event=new Event(); + event.startTime=eventStartTime; + event.endTime=eventEndTime; + attachment=List.of(event); + } } @Override @@ -96,6 +111,10 @@ public JsonObject asActivityPubObject(JsonObject obj, ContextCollector contextCo return obj; } + public boolean isEvent(){ + return type==Type.EVENT; + } + public enum AdminLevel{ REGULAR, MODERATOR, diff --git a/src/main/java/smithereen/jsonld/JLDProcessor.java b/src/main/java/smithereen/jsonld/JLDProcessor.java index 260c2678..3561febd 100644 --- a/src/main/java/smithereen/jsonld/JLDProcessor.java +++ b/src/main/java/smithereen/jsonld/JLDProcessor.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.Objects; public class JLDProcessor{ private static final Logger LOG=LoggerFactory.getLogger(JLDProcessor.class); @@ -141,11 +142,7 @@ private static JsonObject idAndContainerObject(String id, String container){ private static String readResourceFile(String name){ try{ - InputStream in=JLDProcessor.class.getResourceAsStream("/jsonld-schemas/"+name+".jsonld"); - byte[] buf=new byte[in.available()]; - in.read(buf); - in.close(); - return new String(buf, StandardCharsets.UTF_8); + return new String(Objects.requireNonNull(JLDProcessor.class.getResourceAsStream("/jsonld-schemas/"+name+".jsonld")).readAllBytes(), StandardCharsets.UTF_8); }catch(IOException x){ return null; } @@ -157,24 +154,18 @@ private static JsonObject dereferenceContext(String iri){ } if(schemaCache.containsKey(iri)) return schemaCache.get(iri); - String file=null; - switch(iri){ - case "https://www.w3.org/ns/activitystreams": - file=readResourceFile("activitystreams"); - break; - case "https://w3id.org/security/v1": - file=readResourceFile("w3-security"); - break; - case "https://w3id.org/identity/v1": - file=readResourceFile("w3-identity"); - break; - case "https://example.com/schemas/litepub-0.1.jsonld": - file=readResourceFile("litepub-0.1"); - break; - default: + String file=switch(iri){ + case "https://www.w3.org/ns/activitystreams" -> readResourceFile("activitystreams"); + case "https://w3id.org/security/v1" -> readResourceFile("w3-security"); + case "https://w3id.org/identity/v1" -> readResourceFile("w3-identity"); + case "https://example.com/schemas/litepub-0.1.jsonld" -> readResourceFile("litepub-0.1"); + default -> { LOG.warn("Can't dereference remote context '{}'", iri); - //throw new JLDException("loading remote context failed"); - } + yield null; + } + + //throw new JLDException("loading remote context failed"); + }; if(file!=null){ JsonObject obj=JsonParser.parseString(file).getAsJsonObject(); schemaCache.put(iri, obj); diff --git a/src/main/java/smithereen/lang/Lang.java b/src/main/java/smithereen/lang/Lang.java index 452e52dc..957d83fb 100644 --- a/src/main/java/smithereen/lang/Lang.java +++ b/src/main/java/smithereen/lang/Lang.java @@ -160,6 +160,12 @@ public String get(String key, Map formatArgs){ } public void inflected(StringBuilder out, User.Gender gender, String first, String last, Inflector.Case _case){ + if(inflector==null){ + out.append(first); + if(StringUtils.isNotEmpty(last)) + out.append(' ').append(last); + return; + } if(gender==null || gender==User.Gender.UNKNOWN){ gender=inflector.detectGender(first, last, null); } @@ -170,7 +176,7 @@ public void inflected(StringBuilder out, User.Gender gender, String first, Strin }else{ out.append(inflector.isInflectable(first) ? inflector.inflectNamePart(first, Inflector.NamePart.FIRST, gender, _case) : first); if(StringUtils.isNotEmpty(last)) - out.append(' ').append(inflector.isInflectable(last) ? inflector.inflectNamePart(last, Inflector.NamePart.LAST, gender, _case) : first); + out.append(' ').append(inflector.isInflectable(last) ? inflector.inflectNamePart(last, Inflector.NamePart.LAST, gender, _case) : last); } } diff --git a/src/main/java/smithereen/routes/GroupsRoutes.java b/src/main/java/smithereen/routes/GroupsRoutes.java index 8b9cb4de..d8348278 100644 --- a/src/main/java/smithereen/routes/GroupsRoutes.java +++ b/src/main/java/smithereen/routes/GroupsRoutes.java @@ -7,13 +7,23 @@ import java.net.URI; import java.net.URLEncoder; import java.sql.SQLException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TimeZone; import java.util.stream.Collectors; +import smithereen.activitypub.objects.PropertyValue; +import smithereen.controllers.GroupsController; import smithereen.data.ForeignUser; import smithereen.data.PaginatedList; import smithereen.data.SizedImage; @@ -84,11 +94,31 @@ public static Object myManagedGroups(Request req, Response resp, Account self){ return model; } + public static Object myEvents(Request req, Response resp, Account self){ + return myEvents(req, resp, self, GroupsController.EventsType.FUTURE); + } + + public static Object myPastEvents(Request req, Response resp, Account self){ + return myEvents(req, resp, self, GroupsController.EventsType.PAST); + } + + public static Object myEvents(Request req, Response resp, Account self, GroupsController.EventsType type){ + jsLangKey(req, "cancel", "create"); + RenderedTemplateResponse model=new RenderedTemplateResponse("groups", req).with("events", true).with("tab", type==GroupsController.EventsType.PAST ? "past" : "events").with("owner", self.user).pageTitle(lang(req).get("events")); + model.paginate(context(req).getGroupsController().getUserEvents(self.user, type, offset(req), 100)); + return model; + } + public static Object createGroup(Request req, Response resp, Account self){ RenderedTemplateResponse model=new RenderedTemplateResponse("create_group", req); return wrapForm(req, resp, "create_group", "/my/groups/create", lang(req).get("create_group_title"), "create", model); } + public static Object createEvent(Request req, Response resp, Account self){ + RenderedTemplateResponse model=new RenderedTemplateResponse("create_event", req); + return wrapForm(req, resp, "create_event", "/my/groups/create?type=event", lang(req).get("create_event_title"), "create", model); + } + private static Object groupCreateError(Request req, Response resp, String errKey){ if(isAjax(req)){ return new WebDeltaResponse(resp).show("formMessage_createGroup").setContent("formMessage_createGroup", lang(req).get(errKey)); @@ -101,25 +131,22 @@ private static Object groupCreateError(Request req, Response resp, String errKey public static Object doCreateGroup(Request req, Response resp, Account self){ String name=req.queryParams("name"); String description=req.queryParams("description"); - String eventTime=req.queryParams("event_start_time"); - String eventDate=req.queryParams("event_start_date"); -// -// if(!isValidUsername(username)) -// return groupCreateError(req, resp, "err_group_invalid_username"); -// if(isReservedUsername(username)) -// return groupCreateError(req, resp, "err_group_reserved_username"); -// -// final int[] id={0}; -// boolean r=DatabaseUtils.runWithUniqueUsername(username, ()->{ -// id[0]=GroupStorage.createGroup(name, username, self.user.id); -// }); -// -// if(r){ -// ActivityPubWorker.getInstance().sendAddToGroupsCollectionActivity(self.user, GroupStorage.getById(id[0])); -// }else{ -// return groupCreateError(req, resp, "err_group_username_taken"); -// } - Group group=context(req).getGroupsController().createGroup(self.user, name, description); + Group group; + if("event".equals(req.queryParams("type"))){ + String eventTime=req.queryParams("event_start_time"); + String eventDate=req.queryParams("event_start_date"); + if(StringUtils.isEmpty(eventDate) || StringUtils.isEmpty(eventTime)) + throw new BadRequestException("date/time empty"); + + try{ + Instant eventStart=instantFromDateAndTime(req, eventDate, eventTime); + group=context(req).getGroupsController().createEvent(self.user, name, description, eventStart, null); + }catch(DateTimeParseException x){ + throw new BadRequestException(x); + } + }else{ + group=context(req).getGroupsController().createGroup(self.user, name, description); + } if(isAjax(req)){ return new WebDeltaResponse(resp).replaceLocation("/"+group.username); }else{ @@ -185,7 +212,19 @@ public static Object groupProfile(Request req, Response resp, Group group){ model.with("moreMetaTags", Map.of("description", descr)); } model.with("activityPubURL", group.activityPubID); - model.addNavBarItem(l.get("open_group")); + model.addNavBarItem(l.get(switch(group.type){ + case GROUP -> "open_group"; + case EVENT -> "open_event"; + })); + ArrayList profileFields=new ArrayList<>(); + if(StringUtils.isNotEmpty(group.summary)) + profileFields.add(new PropertyValue(l.get(group.type==Group.Type.EVENT ? "about_event" : "about_group"), group.summary)); + if(group.type==Group.Type.EVENT){ + profileFields.add(new PropertyValue(l.get("event_start_time"), l.formatDate(group.eventStartTime, timeZoneForRequest(req), false))); + if(group.eventEndTime!=null) + profileFields.add(new PropertyValue(l.get("event_end_time"), l.formatDate(group.eventEndTime, timeZoneForRequest(req), false))); + } + model.with("profileFields", profileFields); return model; } @@ -242,22 +281,40 @@ public static Object editGeneral(Request req, Response resp, Account self){ return model; } - public static Object saveGeneral(Request req, Response resp, Account self) throws SQLException{ - Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.ADMIN); + public static Object saveGeneral(Request req, Response resp, Account self){ + Group group=getGroup(req); String name=req.queryParams("name"), about=req.queryParams("about"); String message; - if(StringUtils.isEmpty(name) || name.length()<1){ - message=lang(req).get("group_name_too_short"); - }else{ + try{ + if(StringUtils.isEmpty(name) || name.length()<1) + throw new BadRequestException(lang(req).get("group_name_too_short")); + + Instant eventStart=null, eventEnd=null; + if(group.isEvent()){ + String startTime=req.queryParams("event_start_time"), startDate=req.queryParams("event_start_date"); + String endTime=req.queryParams("event_end_time"), endDate=req.queryParams("event_end_date"); + if(StringUtils.isEmpty(startTime) || StringUtils.isEmpty(startDate)) + throw new BadRequestException("start date/time empty"); + try{ + eventStart=instantFromDateAndTime(req, startDate, startTime); + if(StringUtils.isNotEmpty(endDate) && StringUtils.isNotEmpty(endTime)) + eventEnd=instantFromDateAndTime(req, endDate, endTime); + }catch(DateTimeParseException x){ + throw new BadRequestException(x); + } + if(eventEnd!=null && eventStart.isAfter(eventEnd)) + throw new BadRequestException(lang(req).get("err_event_end_time_before_start")); + } + if(StringUtils.isEmpty(about)) about=null; - else - about=preprocessPostHTML(about, null); - GroupStorage.updateGroupGeneralInfo(group, name, about); - message=lang(req).get("group_info_updated"); + + context(req).getGroupsController().updateGroupInfo(group, self.user, name, about, eventStart, eventEnd); + + message=lang(req).get(group.isEvent() ? "event_info_updated" : "group_info_updated"); + }catch(BadRequestException x){ + message=x.getMessage(); } - group=GroupStorage.getById(group.id); - ActivityPubWorker.getInstance().sendUpdateGroupActivity(group); if(isAjax(req)){ return new WebDeltaResponse(resp).show("formMessage_groupEdit").setContent("formMessage_groupEdit", message); } @@ -286,7 +343,7 @@ public static Object admins(Request req, Response resp){ RenderedTemplateResponse model=new RenderedTemplateResponse("group_admins", req); model.with("admins", context(req).getGroupsController().getAdmins(group)); if(isAjax(req)){ - return new WebDeltaResponse(resp).box(lang(req).get("group_admins"), model.renderContentBlock(), null, true); + return new WebDeltaResponse(resp).box(lang(req).get(group.isEvent() ? "event_organizers" : "group_admins"), model.renderContentBlock(), null, true); } return model; } diff --git a/src/main/java/smithereen/routes/SettingsAdminRoutes.java b/src/main/java/smithereen/routes/SettingsAdminRoutes.java index d0cfb4e6..de153b02 100644 --- a/src/main/java/smithereen/routes/SettingsAdminRoutes.java +++ b/src/main/java/smithereen/routes/SettingsAdminRoutes.java @@ -10,6 +10,7 @@ import smithereen.Mailer; import smithereen.Utils; import smithereen.data.Account; +import smithereen.data.PaginatedList; import smithereen.data.User; import smithereen.data.WebDeltaResponse; import smithereen.exceptions.ObjectNotFoundException; @@ -78,10 +79,8 @@ public static Object users(Request req, Response resp, Account self) throws SQLE Lang l=lang(req); int offset=parseIntOrDefault(req.queryParams("offset"), 0); List accounts=UserStorage.getAllAccounts(offset, 100); - model.with("accounts", accounts); + model.paginate(new PaginatedList<>(accounts, UserStorage.getLocalUserCount(), offset, 100)); model.with("title", l.get("admin_users")+" | "+l.get("menu_admin")).with("toolbarTitle", l.get("menu_admin")); - model.with("total", UserStorage.getLocalUserCount()); - model.with("pageOffset", offset); model.with("wideOnDesktop", true); jsLangKey(req, "cancel", "yes", "no"); return model; diff --git a/src/main/java/smithereen/storage/GroupStorage.java b/src/main/java/smithereen/storage/GroupStorage.java index a77a002d..66a8bf8a 100644 --- a/src/main/java/smithereen/storage/GroupStorage.java +++ b/src/main/java/smithereen/storage/GroupStorage.java @@ -12,6 +12,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -19,6 +20,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -28,6 +30,7 @@ import smithereen.LruCache; import smithereen.Utils; import smithereen.activitypub.ContextCollector; +import smithereen.controllers.GroupsController; import smithereen.data.ForeignGroup; import smithereen.data.Group; import smithereen.data.GroupAdmin; @@ -43,7 +46,7 @@ public class GroupStorage{ private static final Object adminUpdateLock=new Object(); - public static int createGroup(String name, String description, String descriptionSrc, int userID, boolean isEvent) throws SQLException{ + public static int createGroup(String name, String description, String descriptionSrc, int userID, boolean isEvent, Instant eventStart, Instant eventEnd) throws SQLException{ int id; Connection conn=DatabaseConnectionManager.getConnection(); try{ @@ -65,6 +68,8 @@ public static int createGroup(String name, String description, String descriptio .value("member_count", 1) .value("about", description) .value("about_source", descriptionSrc) + .value("event_start_time", eventStart) + .value("event_end_time", eventEnd) .value("type", isEvent ? Group.Type.EVENT : Group.Type.GROUP); stmt=bldr.createStatement(Statement.RETURN_GENERATED_KEYS); @@ -427,21 +432,32 @@ public static void leaveGroup(Group group, int userID, boolean tentative) throws public static PaginatedList getUserGroups(int userID, int offset, int count) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); - PreparedStatement stmt=new SQLQueryBuilder(conn) - .selectFrom("group_memberships") - .count() - .where("user_id=? AND accepted=1", userID) - .createStatement(); + String query="SELECT %s FROM group_memberships JOIN `groups` ON group_id=`groups`.id WHERE user_id=? AND accepted=1 AND `groups`.`type`=0"; + PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, String.format(Locale.US, query, "COUNT(*)"), userID); int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery()); if(total==0) return new PaginatedList<>(Collections.emptyList(), 0, 0, count); - stmt=new SQLQueryBuilder() - .selectFrom("group_memberships") - .columns("group_id") - .where("user_id=? AND accepted=1", userID) - .orderBy("group_id ASC") - .limit(count, offset) - .createStatement(); + query+=" ORDER BY group_id ASC LIMIT ? OFFSET ?"; + stmt=SQLQueryBuilder.prepareStatement(conn, String.format(Locale.US, query, "group_id"), userID, count, offset); + try(ResultSet res=stmt.executeQuery()){ + return new PaginatedList<>(getByIdAsList(DatabaseUtils.intResultSetToList(res)), total, offset, count); + } + } + + public static PaginatedList getUserEvents(int userID, GroupsController.EventsType type, int offset, int count) throws SQLException{ + String query="SELECT %s FROM group_memberships JOIN `groups` ON group_id=`groups`.id WHERE user_id=? AND accepted=1 AND `groups`.`type`=1"; + query+=switch(type){ + case PAST -> " AND event_start_time<=CURRENT_TIMESTAMP()"; + case FUTURE -> " AND event_start_time>CURRENT_TIMESTAMP()"; + case ALL -> ""; + }; + Connection conn=DatabaseConnectionManager.getConnection(); + PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, String.format(Locale.US, query, "COUNT(*)"), userID); + int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery()); + if(total==0) + return PaginatedList.emptyList(count); + query+=" ORDER BY event_start_time "+(type==GroupsController.EventsType.PAST ? "DESC" : "ASC")+" LIMIT ? OFFSET ?"; + stmt=SQLQueryBuilder.prepareStatement(conn, String.format(Locale.US, query, "group_id"), userID, count, offset); try(ResultSet res=stmt.executeQuery()){ return new PaginatedList<>(getByIdAsList(DatabaseUtils.intResultSetToList(res)), total, offset, count); } @@ -702,11 +718,14 @@ public static void updateProfilePicture(Group group, String serializedPic) throw } } - public static void updateGroupGeneralInfo(Group group, String name, String about) throws SQLException{ + public static void updateGroupGeneralInfo(Group group, String name, String aboutSrc, String about, Instant eventStart, Instant eventEnd) throws SQLException{ new SQLQueryBuilder() .update("groups") .value("name", name) + .value("about_source", aboutSrc) .value("about", about) + .value("event_start_time", eventStart) + .value("event_end_time", eventEnd) .where("id=?", group.id) .createStatement() .execute(); diff --git a/src/main/java/smithereen/storage/PostStorage.java b/src/main/java/smithereen/storage/PostStorage.java index 66e456c8..d29cb9a2 100644 --- a/src/main/java/smithereen/storage/PostStorage.java +++ b/src/main/java/smithereen/storage/PostStorage.java @@ -590,7 +590,7 @@ public static Map> getRepliesForFeed(Set p public static List getReplies(int[] prefix) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); - PreparedStatement stmt=conn.prepareStatement("SELECT * FROM `wall_posts` WHERE `reply_key` LIKE BINARY bin_prefix(?) ESCAPE CHAR(255) ORDER BY `reply_key` ASC, `id` ASC LIMIT 100"); + PreparedStatement stmt=conn.prepareStatement("SELECT * FROM `wall_posts` WHERE `reply_key` LIKE BINARY bin_prefix(?) ESCAPE CHAR(255) ORDER BY `reply_key` ASC, `created_at` ASC LIMIT 100"); byte[] replyKey; ByteArrayOutputStream b=new ByteArrayOutputStream(prefix.length*4); try{ @@ -639,7 +639,7 @@ public static List getRepliesExact(int[] replyKey, int maxID, int limit, i .allColumns() .where("reply_key=? AND id posts=new ArrayList<>(); diff --git a/src/main/java/smithereen/storage/SessionStorage.java b/src/main/java/smithereen/storage/SessionStorage.java index 941052e5..8ac9bba6 100644 --- a/src/main/java/smithereen/storage/SessionStorage.java +++ b/src/main/java/smithereen/storage/SessionStorage.java @@ -17,6 +17,7 @@ import java.sql.Statement; import java.sql.Timestamp; import java.sql.Types; +import java.time.Instant; import java.util.Base64; import java.util.Locale; import java.util.Objects; @@ -387,7 +388,7 @@ public static boolean updatePassword(int accountID, String newPassword) throws S return false; } - public static void setLastActive(int accountID, String psid, Timestamp time) throws SQLException{ + public static void setLastActive(int accountID, String psid, Instant time) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); new SQLQueryBuilder(conn) .update("accounts") diff --git a/src/main/java/smithereen/templates/InstantToDateFunction.java b/src/main/java/smithereen/templates/InstantToDateFunction.java new file mode 100644 index 00000000..b9575cba --- /dev/null +++ b/src/main/java/smithereen/templates/InstantToDateFunction.java @@ -0,0 +1,28 @@ +package smithereen.templates; + +import com.mitchellbosecke.pebble.extension.Function; +import com.mitchellbosecke.pebble.template.EvaluationContext; +import com.mitchellbosecke.pebble.template.PebbleTemplate; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +public class InstantToDateFunction implements Function{ + + @Override + public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber){ + Instant instant=(Instant) args.get("instant"); + if(instant==null) + return ""; + TimeZone tz=Templates.getVariableRegardless(context, "timeZone"); + return LocalDate.ofInstant(instant, tz.toZoneId()).toString(); + } + + @Override + public List getArgumentNames(){ + return List.of("instant"); + } +} diff --git a/src/main/java/smithereen/templates/InstantToTimeFunction.java b/src/main/java/smithereen/templates/InstantToTimeFunction.java new file mode 100644 index 00000000..179f1db6 --- /dev/null +++ b/src/main/java/smithereen/templates/InstantToTimeFunction.java @@ -0,0 +1,29 @@ +package smithereen.templates; + +import com.mitchellbosecke.pebble.extension.Function; +import com.mitchellbosecke.pebble.template.EvaluationContext; +import com.mitchellbosecke.pebble.template.PebbleTemplate; + +import java.time.Instant; +import java.time.LocalTime; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +public class InstantToTimeFunction implements Function{ + @Override + public Object execute(Map args, PebbleTemplate self, EvaluationContext context, int lineNumber){ + Instant instant=(Instant) args.get("instant"); + if(instant==null) + return ""; + TimeZone tz=Templates.getVariableRegardless(context, "timeZone"); + LocalTime time=LocalTime.ofInstant(instant, tz.toZoneId()); + return String.format(Locale.US, "%02d:%02d", time.getHour(), time.getMinute()); + } + + @Override + public List getArgumentNames(){ + return List.of("instant"); + } +} diff --git a/src/main/java/smithereen/templates/SmithereenExtension.java b/src/main/java/smithereen/templates/SmithereenExtension.java index c6ea21a1..d871a670 100644 --- a/src/main/java/smithereen/templates/SmithereenExtension.java +++ b/src/main/java/smithereen/templates/SmithereenExtension.java @@ -18,7 +18,9 @@ public Map getFunctions(){ "LD", new LangDateFunction(), "renderAttachments", new RenderAttachmentsFunction(), "json", new JsonFunction(), - "formatTime", new FormatTimeFunction() + "formatTime", new FormatTimeFunction(), + "getTime", new InstantToTimeFunction(), + "getDate", new InstantToDateFunction() ); } diff --git a/src/main/java/smithereen/templates/Templates.java b/src/main/java/smithereen/templates/Templates.java index c43b756b..0e17e5f3 100644 --- a/src/main/java/smithereen/templates/Templates.java +++ b/src/main/java/smithereen/templates/Templates.java @@ -4,7 +4,10 @@ import com.mitchellbosecke.pebble.PebbleEngine; import com.mitchellbosecke.pebble.loader.ClasspathLoader; import com.mitchellbosecke.pebble.loader.DelegatingLoader; +import com.mitchellbosecke.pebble.template.EvaluationContext; +import com.mitchellbosecke.pebble.template.EvaluationContextImpl; import com.mitchellbosecke.pebble.template.PebbleTemplate; +import com.mitchellbosecke.pebble.template.Scope; import java.sql.SQLException; import java.time.LocalDate; @@ -121,4 +124,19 @@ else if(req.attribute("mobile")!=null) return (int)(long)(Long)o; throw new IllegalArgumentException("Can't cast "+o+" to int"); } + + /*package*/ static T getVariableRegardless(EvaluationContext context, String key){ + Object result=context.getVariable(key); + if(result!=null) + return (T)result; + if(context instanceof EvaluationContextImpl contextImpl){ + List scopes=contextImpl.getScopeChain().getGlobalScopes(); + for(Scope scope:scopes){ + result=scope.get(key); + if(result!=null) + return (T)result; + } + } + return null; + } } diff --git a/src/main/resources/langs/en.json b/src/main/resources/langs/en.json index 92cea05d..062be14f 100644 --- a/src/main/resources/langs/en.json +++ b/src/main/resources/langs/en.json @@ -458,5 +458,16 @@ "summary_own_X_follows": "You {count, plural, =0 {don''t follow anyone} one {follow # person} other {follow # people}}", "summary_user_X_follows": "{name} {count, plural, =0 {doesn''t follow anyone} one {follows # person} other {follows # people}}", "no_followers": "There are no followers", - "no_follows": "There are no follows" + "no_follows": "There are no follows", + "no_groups": "There are no groups", + "no_events": "There are no events", + "events": "Events", + "summary_X_upcoming_events": "You {count, select, 0 {don''t have any upcoming events} other {are going to attend {count, plural, one {# event} other {# events}}}}", + "summary_X_past_events": "You have {count, plural, =0 {no past events} one {# past event} other {# past events}}", + "date_time_separator": "at", + "write_on_event_wall": "Write on event wall...", + "leave_event": "Leave event", + "event_organizers": "Organizers", + "event_X_organizers": "{count, plural, one {# organizer} other {# organizers}}", + "err_event_end_time_before_start": "Event end time must be after the start time." } \ No newline at end of file diff --git a/src/main/resources/langs/ru.json b/src/main/resources/langs/ru.json index 5209b5b3..6c0e0edb 100644 --- a/src/main/resources/langs/ru.json +++ b/src/main/resources/langs/ru.json @@ -460,5 +460,16 @@ "summary_own_X_follows": "Вы {count, select, 0 {не подписались ни на чьи обновления} other {подписались на обновления {count, plural, one {# человека} other {# человек}}}}", "summary_user_X_follows": "{name} {count, select, 0 {не {gender, select, female {подписалась} other {подписался}} ни на чьи обновления} other {{gender, select, female {подписалась} other {подписался}} на обновления {count, plural, one {# человека} other {# человек}}}}", "no_followers": "Нет ни одного подписчика", - "no_follows": "Нет ни одной подписки" + "no_follows": "Нет ни одной подписки", + "no_groups": "Нет ни одной группы", + "no_events": "Нет ни одной встречи", + "events": "Встречи", + "summary_X_upcoming_events": "{count, select, 0 {У Вас нет предстоящих встреч} other {Вы пойдёте на {count, plural, one {# встречу} other {# встреч}}}}", + "summary_X_past_events": "У Вас {count, plural, =0 {нет прошедших встреч} one {# прошедшая встреча} few {# прошедшие встречи} other {# прошедших встреч}}", + "date_time_separator": "в", + "write_on_event_wall": "Написать на стене встречи...", + "leave_event": "Покинуть встречу", + "event_organizers": "Организаторы", + "event_X_organizers": "{count, plural, one {# организатор} few {# организатора} other {# организаторов}}", + "err_event_end_time_before_start": "Время окончания встречи должно быть после времени начала." } \ No newline at end of file diff --git a/src/main/resources/templates/common/admin_users.twig b/src/main/resources/templates/common/admin_users.twig index ce708a86..5f26def0 100644 --- a/src/main/resources/templates/common/admin_users.twig +++ b/src/main/resources/templates/common/admin_users.twig @@ -10,7 +10,7 @@ {{L('signup_date')}} {{L('actions')}} - {%for acc in accounts%} + {% for acc in items %} {{acc.id}} ({{acc.user.id}}) {{acc.user | pictureForAvatar('s', 32)}}{{acc.user.fullName}} @@ -29,12 +29,10 @@ {% endif %} - {%endfor%} + {% endfor %}
    -{% if total>100 %}
    - {%include "pagination" with {'perPage': 100, 'offset': pageOffset, 'total': total, 'urlPrefix': "/settings/admin/users?offset="}%} + {% include "pagination" %}
    -{% endif %} {%endblock%} \ No newline at end of file diff --git a/src/main/resources/templates/common/create_event.twig b/src/main/resources/templates/common/create_event.twig new file mode 100644 index 00000000..4c9056e9 --- /dev/null +++ b/src/main/resources/templates/common/create_event.twig @@ -0,0 +1,6 @@ +{% import "forms" as form %} +{{ form.start('createGroup', createGroupMessage) }} + {{ form.textInput('name', L('group_name'), groupName, {'maxlength': '200', 'required': true, 'autocomplete': false}) }} + {{ form.textArea('description', L('event_description'), groupDescription) }} + {{ form.dateTimeInput('event_start', L('event_start_time'), eventStartTime, {'required': true}) }} +{{ form.end() }} \ No newline at end of file diff --git a/src/main/resources/templates/common/events_tabbar.twig b/src/main/resources/templates/common/events_tabbar.twig new file mode 100644 index 00000000..6ac489e9 --- /dev/null +++ b/src/main/resources/templates/common/events_tabbar.twig @@ -0,0 +1,5 @@ + diff --git a/src/main/resources/templates/common/group_admin_tabbar.twig b/src/main/resources/templates/common/group_admin_tabbar.twig index fa84249d..03ec2ac1 100644 --- a/src/main/resources/templates/common/group_admin_tabbar.twig +++ b/src/main/resources/templates/common/group_admin_tabbar.twig @@ -7,5 +7,5 @@ {% endif %} {{ L('members') }} {{ L('settings_blocking') }} - {{ L('back_to_group') }} + {{ L(group.event ? 'back_to_event' : 'back_to_group') }}
    \ No newline at end of file diff --git a/src/main/resources/templates/common/group_edit_general.twig b/src/main/resources/templates/common/group_edit_general.twig index afe7beeb..bdc2efe2 100644 --- a/src/main/resources/templates/common/group_edit_general.twig +++ b/src/main/resources/templates/common/group_edit_general.twig @@ -7,7 +7,11 @@ {{ form.start('groupEdit', groupEditMessage) }} {{ form.textInput('name', L('group_name'), group.name, {'maxlength': 200, 'required': true}) }} - {{ form.textArea('about', L('about_group'), group.summary) }} + {{ form.textArea('about', L(group.event ? 'about_event' : 'about_group'), group.summary) }} + {% if group.event %} + {{ form.dateTimeInput('event_start', L('event_start_time'), group.eventStartTime) }} + {{ form.dateTimeInput('event_end', L('event_end_time'), group.eventEndTime) }} + {% endif %} {{ form.footer(L('save')) }} {{ form.end() }} diff --git a/src/main/resources/templates/desktop/forms.twig b/src/main/resources/templates/desktop/forms.twig index 0fa742c2..2e6d768e 100644 --- a/src/main/resources/templates/desktop/forms.twig +++ b/src/main/resources/templates/desktop/forms.twig @@ -44,6 +44,26 @@ {%endmacro%} +{% macro dateTimeInput(name, label, value, options) %} +{%if options is null%}{%set options={}%}{%endif%} + + + : + + +
    + + {{ L('date_time_separator') }} + +
    + + +{% endmacro %} + {%macro textArea(name, label, value, options)%} {%if options is null%}{%set options={}%}{%endif%} diff --git a/src/main/resources/templates/desktop/group.twig b/src/main/resources/templates/desktop/group.twig index 60af45ec..f2f30323 100644 --- a/src/main/resources/templates/desktop/group.twig +++ b/src/main/resources/templates/desktop/group.twig @@ -18,12 +18,15 @@

    {{ group.name }}

    - {% if group.summary is not null %} - - {% endif %} + {% for fld in profileFields %} + + + + + {% endfor %}
    {{ L('about_group') }}:{{ group.summary | postprocessHTML }}
    {{ fld.name }}:{{ fld.value | postprocessHTML }}
    - {% include "profile_module_wall" with {'wallOwner': user, 'isGroup': true, 'headerTitle': L('wall'), 'headerHref': "/groups/#{group.id}/wall", 'additionalHeader': L('X_posts', {'count': totalItems})} %} + {% include "profile_module_wall" with {'wallOwner': group, 'isGroup': true, 'headerTitle': L('wall'), 'headerHref': "/groups/#{group.id}/wall", 'additionalHeader': L('X_posts', {'count': totalItems})} %} @@ -64,20 +67,20 @@
    {% include "profile_module_user_grid" with {'users': members, 'headerTitle': L('members'), 'headerHref': "/groups/#{group.id}/members", 'subheaderTitle': L('X_members', {'count': group.memberCount})} %} - {% include "profile_module_user_list" with {'users': admins, 'headerTitle': L('group_admins'), 'headerHref': "/groups/#{group.id}/admins", 'headerAjaxBox': true, 'subheaderTitle': L('group_X_admins', {'count': admins.size})} %} + {% include "profile_module_user_list" with {'users': admins, 'headerTitle': L(group.event ? 'event_organizers' : 'group_admins'), 'headerHref': "/groups/#{group.id}/admins", 'headerAjaxBox': true, 'subheaderTitle': L(group.event ? 'event_X_organizers' : 'group_X_admins', {'count': admins.size})} %}
    diff --git a/src/main/resources/templates/desktop/groups.twig b/src/main/resources/templates/desktop/groups.twig index 7853ac69..f06f23e5 100644 --- a/src/main/resources/templates/desktop/groups.twig +++ b/src/main/resources/templates/desktop/groups.twig @@ -1,7 +1,7 @@ {% extends "page" %} {% block content %} {% if currentUser is not null and owner.id==currentUser.id %} -{% include 'groups_tabbar' %} +{% include (events ? 'events_tabbar' : 'groups_tabbar') %} {% else %}
    {{ L('user_groups', {'name': owner.firstAndGender}) }} @@ -10,7 +10,9 @@ {% endif %}
    - {% if currentUser is not null and owner.id==currentUser.id %} + {% if events %} + {{ L(tab=='past' ? 'summary_X_past_events' : 'summary_X_upcoming_events', {'count': totalItems}) }} + {% elseif currentUser is not null and owner.id==currentUser.id %} {{ L(tab=='managed' ? 'summary_X_managed_groups' : 'summary_own_X_groups', {'numGroups': totalItems}) }} {% else %} {{ L('summary_user_X_groups', {'name': owner.firstAndGender, 'numGroups': totalItems}) }} @@ -38,7 +40,7 @@ {% else %} -
    {{L('no_groups')}}
    +
    {{ L(events ? 'no_events' : 'no_groups') }}
    {% endfor %}
    {% include "pagination" %}
    diff --git a/src/main/resources/templates/desktop/wall_post_form.twig b/src/main/resources/templates/desktop/wall_post_form.twig index dfe1ef5e..956e8c7d 100644 --- a/src/main/resources/templates/desktop/wall_post_form.twig +++ b/src/main/resources/templates/desktop/wall_post_form.twig @@ -14,7 +14,7 @@ {% if replyTo is not null %} {% set fieldPlaceholder=L('comment_placeholder') %} {% elseif isGroup %} - {% set fieldPlaceholder=L('write_on_group_wall') %} + {% set fieldPlaceholder=L(wallOwner.event ? 'write_on_event_wall' : 'write_on_group_wall') %} {% elseif wallOwner.id!=currentUser.id%} {% set fieldPlaceholder=L('write_on_X_wall', {'name': wallOwner.firstAndGender}) %} {% else %} diff --git a/src/main/resources/templates/mobile/forms.twig b/src/main/resources/templates/mobile/forms.twig index 619d09ad..51efd829 100644 --- a/src/main/resources/templates/mobile/forms.twig +++ b/src/main/resources/templates/mobile/forms.twig @@ -54,6 +54,25 @@ autoSizeTextArea(ge("{{ name }}")); {% endscript %} {%endmacro%} +{% macro dateTimeInput(name, label, value, options) %} + {%if options is null%}{%set options={}%}{%endif%} +
    +
    + : +
    +
    + + {{ L('date_time_separator') }} + +
    +
    +{% endmacro %} + + {%macro select(name, label, selectOptions, options)%} {%if options is null%}{%set options={}%}{%endif%}
    diff --git a/src/main/resources/templates/mobile/group.twig b/src/main/resources/templates/mobile/group.twig index df5f11e1..7d3aa991 100644 --- a/src/main/resources/templates/mobile/group.twig +++ b/src/main/resources/templates/mobile/group.twig @@ -47,22 +47,22 @@
  • {% if currentUser is not null %} {% if membershipState=="MEMBER" %} -
  • {{ L('leave_group') }}
  • +
  • {{ L(group.event ? 'leave_event' : 'leave_group') }}
  • {% endif %} {% endif %} {% if canEditGroup %} -
  • {{ L('edit_group') }}
  • +
  • {{ L(group.event ? 'edit_event' : 'edit_group') }}
  • {% elseif canManageGroup %} -
  • {{ L('manage_group') }}
  • +
  • {{ L(group.event ? 'manage_event' : 'manage_group') }}
  • {% endif %}
    -{% if group.summary is not empty %} -
    {{L('about_group')}}
    -
    {{ group.summary | postprocessHTML }}
    -{% endif %} +{% for fld in profileFields %} +
    {{ fld.name }}
    +
    {{ fld.value | postprocessHTML }}
    +{% endfor %}
    {% include "wall_profile_block" with {'wallOwner': group, 'isGroup': true} %} diff --git a/src/main/resources/templates/mobile/groups.twig b/src/main/resources/templates/mobile/groups.twig index 6d91567c..79bd7dc1 100644 --- a/src/main/resources/templates/mobile/groups.twig +++ b/src/main/resources/templates/mobile/groups.twig @@ -1,7 +1,7 @@ {%extends "page"%} {%block content%} {% if currentUser is not null and owner.id==currentUser.id %} -{% include 'groups_tabbar' %} +{% include (events ? 'events_tabbar' : 'groups_tabbar') %} {% endif %}
    {% for group in items %} @@ -16,7 +16,7 @@ {%else%} -
    {{ L('no_groups') }}
    +
    {{ L(events ? 'no_events' : 'no_groups') }}
    {%endfor%} {% include "pagination" %}
    diff --git a/src/main/resources/templates/mobile/wall_post_form.twig b/src/main/resources/templates/mobile/wall_post_form.twig index e88d2bfa..a65b1a34 100644 --- a/src/main/resources/templates/mobile/wall_post_form.twig +++ b/src/main/resources/templates/mobile/wall_post_form.twig @@ -11,7 +11,7 @@ {% else %} {% if isGroup %} - {% set fieldPlaceholder=L('write_on_group_wall') %} + {% set fieldPlaceholder=L(wallOwner.event ? 'write_on_event_wall' : 'write_on_group_wall') %} {% elseif wallOwner.id!=currentUser.id%} {% set fieldPlaceholder=L('write_on_X_wall', {'name': wallOwner.firstAndGender}) %} {% elseif replyTo is not null %} diff --git a/src/main/web/common.scss b/src/main/web/common.scss index 8632fcbf..47036382 100644 --- a/src/main/web/common.scss +++ b/src/main/web/common.scss @@ -108,6 +108,18 @@ $likeIconColor: rgb(219, 216, 173); fill: $blockBackground; } } +@svg-load dateInputIcon url('img/calendar_input.svg') { + fill: $textAreaBorder; + *[fill=red]{ + fill: $blockBackground; + } +} +@svg-load timeInputIcon url('img/time_input.svg') { + fill: $textAreaBorder; + *[fill=red]{ + fill: $blockBackground; + } +} *{ @@ -293,7 +305,7 @@ a, a:link, a:visited, .link{ }*/ } -input[type=text], input[type=password], input[type=email], input[type=date], input[type=number], textarea, select, .prefixedInput{ +input[type=text], input[type=password], input[type=email], input[type=date], input[type=time], input[type=number], textarea, select, .prefixedInput{ border: solid 1px $textAreaBorder; padding: 3px; appearance: none; @@ -765,3 +777,17 @@ small{ .uppercase{ text-transform: uppercase; } + +.dateTimeInput{ + display: flex; + align-content: center; + input[type=date]{ + flex-grow: 2; + } + input[type=time]{ + flex-grow: 1; + } + input{ + width: unset !important; + } +} diff --git a/src/main/web/desktop.scss b/src/main/web/desktop.scss index 907e8480..b4a21b32 100644 --- a/src/main/web/desktop.scss +++ b/src/main/web/desktop.scss @@ -775,10 +775,26 @@ small{ } } -input[type=date], select{ +input[type=date], input[type=time], select{ height: 21px; } +input[type=date]::-webkit-calendar-picker-indicator{ + background: svg-inline(dateInputIcon) no-repeat center right 2px; + width: 19px; + height: 19px; +} + +input[type=time]::-webkit-calendar-picker-indicator{ + background: svg-inline(timeInputIcon) no-repeat center right 2px; + width: 19px; + height: 19px; +} + +input[type=date], input[type=time]{ + padding-right: 0; +} + *:focus{ outline: none; } @@ -1952,3 +1968,10 @@ label.pretendLink{ .fixLineHeight{ line-height: 13px; } + +.dateTimeInput{ + .separator{ + line-height: 21px; + padding: 0 5px; + } +} diff --git a/src/main/web/img/calendar_input.svg b/src/main/web/img/calendar_input.svg new file mode 100644 index 00000000..f0ff06be --- /dev/null +++ b/src/main/web/img/calendar_input.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/main/web/img/time_input.svg b/src/main/web/img/time_input.svg new file mode 100644 index 00000000..a8b5c4e0 --- /dev/null +++ b/src/main/web/img/time_input.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/web/mobile.scss b/src/main/web/mobile.scss index 00735eba..d94fa853 100644 --- a/src/main/web/mobile.scss +++ b/src/main/web/mobile.scss @@ -181,7 +181,7 @@ input[type=submit], input[type=button], button, .button, .button:link, .button:v } } -input[type=text], input[type=password], input[type=email], input[type=date], textarea, select{ +input[type=text], input[type=password], input[type=email], input[type=date], input[type=time], textarea, select{ border-radius: 4px; padding: 8px; } @@ -1261,3 +1261,18 @@ textarea{ } } } + +.dateTimeInput{ + .separator{ + line-height: 40px; + padding: 0 10px; + } +} + +input[type=date]::-webkit-calendar-picker-indicator{ + background: svg-inline(dateInputIcon) no-repeat center; +} + +input[type=time]::-webkit-calendar-picker-indicator{ + background: svg-inline(timeInputIcon) no-repeat center; +} diff --git a/src/main/web/package-lock.json b/src/main/web/package-lock.json index 46da1825..020259b4 100644 --- a/src/main/web/package-lock.json +++ b/src/main/web/package-lock.json @@ -165,9 +165,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001228", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz", - "integrity": "sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==" + "version": "1.0.30001286", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001286.tgz", + "integrity": "sha512-zaEMRH6xg8ESMi2eQ3R4eZ5qw/hJiVsO/HlLwniIwErij0JDr9P+8V4dtx1l+kLq6j3yy8l8W4fst1lBnat5wQ==" }, "chalk": { "version": "2.4.2", @@ -824,9 +824,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "nanoid": { - "version": "3.1.23", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", - "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==" + "version": "3.1.30", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", + "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==" }, "node-releases": { "version": "1.1.72", @@ -890,6 +890,11 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, "picomatch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", @@ -901,13 +906,13 @@ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" }, "postcss": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.0.tgz", - "integrity": "sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==", + "version": "8.4.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.4.tgz", + "integrity": "sha512-joU6fBsN6EIer28Lj6GDFoC/5yOZzLCfn0zHAn/MYXI7aPt4m4hK5KC5ovEZXy+lnCjmYIbQWngvju2ddyEr8Q==", "requires": { - "colorette": "^1.2.2", - "nanoid": "^3.1.23", - "source-map-js": "^0.6.2" + "nanoid": "^3.1.30", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.1" } }, "postcss-advanced-variables": { @@ -1497,9 +1502,9 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", - "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz", + "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==" }, "stable": { "version": "0.1.8", @@ -1614,9 +1619,9 @@ } }, "typescript": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", - "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==" + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.3.tgz", + "integrity": "sha512-eVYaEHALSt+s9LbvgEv4Ef+Tdq7hBiIZgii12xXJnukryt3pMgJf6aKhoCZ3FWQsu6sydEnkg11fYXLzhLBjeQ==" }, "uglify-js": { "version": "3.13.7", diff --git a/src/main/web/package.json b/src/main/web/package.json index 94add0d8..0ddd2296 100644 --- a/src/main/web/package.json +++ b/src/main/web/package.json @@ -2,15 +2,15 @@ "dependencies": { "autoprefixer": "latest", "cssnano": "latest", + "postcss": "^8.4.4", "postcss-advanced-variables": "latest", + "postcss-cli": "latest", "postcss-color-function": "latest", "postcss-import": "latest", "postcss-inline-svg": "latest", "postcss-nested": "latest", "postcss-rgba-hex": "latest", - "postcss-cli": "latest", - "postcss": "latest", - "typescript": "latest", + "typescript": "^4.5.3", "uglify-js": "latest" } } diff --git a/src/test/java/smithereen/HTMLSanitizerTest.java b/src/test/java/smithereen/HTMLSanitizerTest.java index b79c08fb..8e7a7bc6 100644 --- a/src/test/java/smithereen/HTMLSanitizerTest.java +++ b/src/test/java/smithereen/HTMLSanitizerTest.java @@ -1,6 +1,7 @@ package smithereen; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.net.URI; @@ -9,8 +10,7 @@ public class HTMLSanitizerTest{ - @BeforeAll - public static void setupConfig(){ + public HTMLSanitizerTest(){ Config.domain="smithereen.local"; } From 3fac8a0039fec3c4cab36633e2d2278325e44fe8 Mon Sep 17 00:00:00 2001 From: Grishka Date: Wed, 15 Dec 2021 14:27:09 +0300 Subject: [PATCH 12/65] update npm --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8da3379a..5109ddc5 100644 --- a/pom.xml +++ b/pom.xml @@ -114,7 +114,7 @@ https://repo1.maven.org/maven2/com/github/eirslett/frontend-maven-plugin/ --> 1.12.0 - v14.17.0 + v16.13.1 src/main/web target From ec4f9368a6c9a88917f5faf55c04d40b8e1e9315 Mon Sep 17 00:00:00 2001 From: Grishka Date: Wed, 15 Dec 2021 14:27:59 +0300 Subject: [PATCH 13/65] add joined event and created group/event newsfeed entries --- .../activitypub/handlers/AddGroupHandler.java | 2 +- .../handlers/RemoveGroupHandler.java | 2 +- .../controllers/GroupsController.java | 3 + .../data/feed/JoinEventNewsfeedEntry.java | 7 + .../smithereen/data/feed/NewsfeedEntry.java | 10 + .../java/smithereen/routes/GroupsRoutes.java | 4 +- .../java/smithereen/storage/PostStorage.java | 10 +- src/main/resources/langs/en.json | 10 +- src/main/resources/langs/ru.json | 8 +- .../resources/templates/common/feed_row.twig | 46 + .../resources/templates/desktop/feed.twig | 41 +- src/main/resources/templates/mobile/feed.twig | 39 +- src/main/web/common.scss | 3 + src/main/web/package-lock.json | 2476 ++++++++++++++++- 14 files changed, 2568 insertions(+), 93 deletions(-) create mode 100644 src/main/java/smithereen/data/feed/JoinEventNewsfeedEntry.java create mode 100644 src/main/resources/templates/common/feed_row.twig diff --git a/src/main/java/smithereen/activitypub/handlers/AddGroupHandler.java b/src/main/java/smithereen/activitypub/handlers/AddGroupHandler.java index 8e08d758..0d83de75 100644 --- a/src/main/java/smithereen/activitypub/handlers/AddGroupHandler.java +++ b/src/main/java/smithereen/activitypub/handlers/AddGroupHandler.java @@ -29,7 +29,7 @@ public void handle(ActivityHandlerContext context, ForeignUser actor, Add activi // TODO verify that this user is actually a member of this group and store the membership // https://socialhub.activitypub.rocks/t/querying-activitypub-collections/1866 - NewsfeedStorage.putEntry(actor.id, object.id, NewsfeedEntry.Type.JOIN_GROUP, null); + NewsfeedStorage.putEntry(actor.id, object.id, object.isEvent() ? NewsfeedEntry.Type.JOIN_EVENT : NewsfeedEntry.Type.JOIN_GROUP, null); }else{ LOG.warn("Unknown Add{Group} target {}", target); } diff --git a/src/main/java/smithereen/activitypub/handlers/RemoveGroupHandler.java b/src/main/java/smithereen/activitypub/handlers/RemoveGroupHandler.java index f447fd2f..3bb0d0ce 100644 --- a/src/main/java/smithereen/activitypub/handlers/RemoveGroupHandler.java +++ b/src/main/java/smithereen/activitypub/handlers/RemoveGroupHandler.java @@ -20,7 +20,7 @@ public void handle(ActivityHandlerContext context, ForeignUser actor, Remove act throw new BadRequestException("activity.target is required and must be a URI"); URI target=activity.target.link; if(target.equals(actor.getFriendsURL())){ - NewsfeedStorage.deleteEntry(actor.id, object.id, NewsfeedEntry.Type.JOIN_GROUP); + NewsfeedStorage.deleteEntry(actor.id, object.id, object.isEvent() ? NewsfeedEntry.Type.JOIN_EVENT : NewsfeedEntry.Type.JOIN_GROUP); }else{ LOG.warn("Unknown Remove{Group} target {}", target); } diff --git a/src/main/java/smithereen/controllers/GroupsController.java b/src/main/java/smithereen/controllers/GroupsController.java index b498c90f..dd94de50 100644 --- a/src/main/java/smithereen/controllers/GroupsController.java +++ b/src/main/java/smithereen/controllers/GroupsController.java @@ -18,10 +18,12 @@ import smithereen.data.GroupAdmin; import smithereen.data.PaginatedList; import smithereen.data.User; +import smithereen.data.feed.NewsfeedEntry; import smithereen.exceptions.InternalServerErrorException; import smithereen.exceptions.ObjectNotFoundException; import smithereen.exceptions.UserActionNotAllowedException; import smithereen.storage.GroupStorage; +import smithereen.storage.NewsfeedStorage; import spark.utils.StringUtils; public class GroupsController{ @@ -51,6 +53,7 @@ private Group createGroupInternal(User admin, String name, String description, b int id=GroupStorage.createGroup(name, Utils.preprocessPostHTML(description, null), description, admin.id, isEvent, startTime, endTime); Group group=Objects.requireNonNull(GroupStorage.getById(id)); ActivityPubWorker.getInstance().sendAddToGroupsCollectionActivity(admin, group); + NewsfeedStorage.putEntry(admin.id, group.id, isEvent ? NewsfeedEntry.Type.CREATE_EVENT : NewsfeedEntry.Type.CREATE_GROUP, null); return group; }catch(SQLException x){ throw new InternalServerErrorException(x); diff --git a/src/main/java/smithereen/data/feed/JoinEventNewsfeedEntry.java b/src/main/java/smithereen/data/feed/JoinEventNewsfeedEntry.java new file mode 100644 index 00000000..623299a0 --- /dev/null +++ b/src/main/java/smithereen/data/feed/JoinEventNewsfeedEntry.java @@ -0,0 +1,7 @@ +package smithereen.data.feed; + +import smithereen.data.Group; + +public class JoinEventNewsfeedEntry extends NewsfeedEntry{ + public Group event; +} diff --git a/src/main/java/smithereen/data/feed/NewsfeedEntry.java b/src/main/java/smithereen/data/feed/NewsfeedEntry.java index a2dc0ece..1a9b2509 100644 --- a/src/main/java/smithereen/data/feed/NewsfeedEntry.java +++ b/src/main/java/smithereen/data/feed/NewsfeedEntry.java @@ -24,6 +24,10 @@ public String toString(){ '}'; } + public boolean isNonPost(){ + return type!=Type.POST && type!=Type.RETOOT; + } + public enum Type{ /** * New post. objectID is a post @@ -41,5 +45,11 @@ public enum Type{ * Someone joined a group. objectID is a group */ JOIN_GROUP, + /** + * Someone joined an event. objectID is a group + */ + JOIN_EVENT, + CREATE_GROUP, + CREATE_EVENT, } } diff --git a/src/main/java/smithereen/routes/GroupsRoutes.java b/src/main/java/smithereen/routes/GroupsRoutes.java index d8348278..89cdb0f9 100644 --- a/src/main/java/smithereen/routes/GroupsRoutes.java +++ b/src/main/java/smithereen/routes/GroupsRoutes.java @@ -241,7 +241,7 @@ public static Object join(Request req, Response resp, Account self) throws SQLEx }else{ ActivityPubWorker.getInstance().sendAddToGroupsCollectionActivity(self.user, group); } - NewsfeedStorage.putEntry(self.user.id, group.id, NewsfeedEntry.Type.JOIN_GROUP, null); + NewsfeedStorage.putEntry(self.user.id, group.id, group.isEvent() ? NewsfeedEntry.Type.JOIN_EVENT : NewsfeedEntry.Type.JOIN_GROUP, null); if(isAjax(req)){ return new WebDeltaResponse(resp).refresh(); } @@ -261,7 +261,7 @@ public static Object leave(Request req, Response resp, Account self) throws SQLE }else{ ActivityPubWorker.getInstance().sendRemoveFromGroupsCollectionActivity(self.user, group); } - NewsfeedStorage.deleteEntry(self.user.id, group.id, NewsfeedEntry.Type.JOIN_GROUP); + NewsfeedStorage.deleteEntry(self.user.id, group.id, group.isEvent() ? NewsfeedEntry.Type.JOIN_EVENT : NewsfeedEntry.Type.JOIN_GROUP); if(isAjax(req)){ return new WebDeltaResponse(resp).refresh(); } diff --git a/src/main/java/smithereen/storage/PostStorage.java b/src/main/java/smithereen/storage/PostStorage.java index d29cb9a2..e8657cef 100644 --- a/src/main/java/smithereen/storage/PostStorage.java +++ b/src/main/java/smithereen/storage/PostStorage.java @@ -36,6 +36,7 @@ import smithereen.data.PollOption; import smithereen.data.UriBuilder; import smithereen.data.feed.AddFriendNewsfeedEntry; +import smithereen.data.feed.JoinEventNewsfeedEntry; import smithereen.data.feed.JoinGroupNewsfeedEntry; import smithereen.exceptions.ObjectNotFoundException; import smithereen.Utils; @@ -319,13 +320,20 @@ public static List getFeed(int userID, int startFromID, int offse _entry.author=UserStorage.getById(res.getInt(3)); yield _entry; } - case JOIN_GROUP -> { + case JOIN_GROUP, CREATE_GROUP -> { JoinGroupNewsfeedEntry _entry=new JoinGroupNewsfeedEntry(); _entry.objectID=res.getInt(2); _entry.group=GroupStorage.getById(_entry.objectID); _entry.author=UserStorage.getById(res.getInt(3)); yield _entry; } + case JOIN_EVENT, CREATE_EVENT -> { + JoinEventNewsfeedEntry _entry=new JoinEventNewsfeedEntry(); + _entry.objectID=res.getInt(2); + _entry.event=GroupStorage.getById(_entry.objectID); + _entry.author=UserStorage.getById(res.getInt(3)); + yield _entry; + } }; entry.type=type; entry.id=res.getInt(4); diff --git a/src/main/resources/langs/en.json b/src/main/resources/langs/en.json index 062be14f..a2dc439e 100644 --- a/src/main/resources/langs/en.json +++ b/src/main/resources/langs/en.json @@ -352,7 +352,7 @@ "feed_added_friend_before": " added ", "feed_added_friend_name": "{name}", "feed_added_friend_after": " as a friend.", - "feed_joined_group_before": " joined group ", + "feed_joined_group_before": " joined the group ", "feed_joined_group_after": ".", "search": "Search", "qsearch_hint": "Start typing a name or paste a link", @@ -469,5 +469,11 @@ "leave_event": "Leave event", "event_organizers": "Organizers", "event_X_organizers": "{count, plural, one {# organizer} other {# organizers}}", - "err_event_end_time_before_start": "Event end time must be after the start time." + "err_event_end_time_before_start": "Event end time must be after the start time.", + "feed_joined_event_before": " is attending the event ", + "feed_joined_event_after": ".", + "feed_created_group_before": " created the group ", + "feed_created_group_after": ".", + "feed_created_event_before": " created the event ", + "feed_created_event_after": "." } \ No newline at end of file diff --git a/src/main/resources/langs/ru.json b/src/main/resources/langs/ru.json index 6c0e0edb..c098abe9 100644 --- a/src/main/resources/langs/ru.json +++ b/src/main/resources/langs/ru.json @@ -471,5 +471,11 @@ "leave_event": "Покинуть встречу", "event_organizers": "Организаторы", "event_X_organizers": "{count, plural, one {# организатор} few {# организатора} other {# организаторов}}", - "err_event_end_time_before_start": "Время окончания встречи должно быть после времени начала." + "err_event_end_time_before_start": "Время окончания встречи должно быть после времени начала.", + "feed_joined_event_before": " примет участие во встрече ", + "feed_joined_event_after": ".", + "feed_created_group_before": " {gender, select, female {создала} other {создал}} группу ", + "feed_created_group_after": ".", + "feed_created_event_before": " {gender, select, female {создала} other {создал}} встречу ", + "feed_created_event_after": "." } \ No newline at end of file diff --git a/src/main/resources/templates/common/feed_row.twig b/src/main/resources/templates/common/feed_row.twig new file mode 100644 index 00000000..6d55c9a5 --- /dev/null +++ b/src/main/resources/templates/common/feed_row.twig @@ -0,0 +1,46 @@ +{# @pebvariable name="entry" type="smithereen.data.feed.NewsfeedEntry" #} +{%if entry.type=="POST"%} +
    + {%include "wall_post" with {'post': entry.post}%} +
    +{%elseif entry.type=="RETOOT"%} +
    +
    +
    {{ formatTime(entry.time) }}
    +
    + {{ L(entry.post.replyLevel==0 ? 'feed_retoot_before' : 'feed_retoot_before_comment', {'gender': entry.author.gender}) -}} + {{entry.author.fullName}} + {{- L(entry.post.replyLevel==0 ? 'feed_retoot_after' : 'feed_retoot_after_comment', {'gender': entry.author.gender}) -}} +
    + {%include "wall_post" with {'post': entry.post}%} +
    +{% elseif entry.nonPost %} +{% if entry.type=="ADD_FRIEND" %} + {% set icon="Add" %} + {% set langKey="added_friend" %} + {% set targetHref=entry.friend.profileURL %} + {% set targetName=L('feed_added_friend_name', {'name': entry.friend.firstLastAndGender}) %} +{% elseif entry.type=="JOIN_GROUP" or entry.type=="CREATE_GROUP" %} + {% set icon="Group" %} + {% set langKey=(entry.type=="CREATE_GROUP" ? "created_group" : "joined_group") %} + {% set targetHref=entry.group.profileURL %} + {% set targetName=entry.group.name %} +{% elseif entry.type=="JOIN_EVENT" or entry.type=="CREATE_EVENT" %} + {% set icon="Event" %} + {% set langKey=(entry.type=="CREATE_EVENT" ? "created_event" : "joined_event") %} + {% set targetHref=entry.event.profileURL %} + {% set targetName=entry.event.name %} +{% endif %} +
    +
    +
    {{ formatTime(entry.time) }}
    +
    + {{ entry.author.completeName }} + {{- L("feed_#{langKey}_before", {'gender': entry.author.gender}) -}} + {{ targetName }} + {{- L("feed_#{langKey}_after", {'gender': entry.author.gender}) -}} +
    +
    +{%else%} +
    Unknown entry type {{ entry.type }}
    +{%endif%} diff --git a/src/main/resources/templates/desktop/feed.twig b/src/main/resources/templates/desktop/feed.twig index f916bdba..d57a4319 100644 --- a/src/main/resources/templates/desktop/feed.twig +++ b/src/main/resources/templates/desktop/feed.twig @@ -8,46 +8,7 @@
    {%for entry in feed%} -{%if entry.type=="POST"%} -
    -{%include "wall_post" with {'post': entry.post}%} -
    -{%elseif entry.type=="RETOOT"%} -
    -
    -
    {{ formatTime(entry.time) }}
    -
    - {{ L(entry.post.replyLevel==0 ? 'feed_retoot_before' : 'feed_retoot_before_comment', {'gender': entry.author.gender}) -}} - {{entry.author.fullName}} - {{- L(entry.post.replyLevel==0 ? 'feed_retoot_after' : 'feed_retoot_after_comment', {'gender': entry.author.gender}) -}} -
    -{%include "wall_post" with {'post': entry.post}%} -
    -{% elseif entry.type=="ADD_FRIEND" %} -
    -
    -
    {{ formatTime(entry.time) }}
    -
    -{{ entry.author.fullName }} - {{- L('feed_added_friend_before', {'gender': entry.author.gender}) -}} - {{ L('feed_added_friend_name', {'name': entry.friend.firstLastAndGender}) }} - {{- L('feed_added_friend_after', {'gender': entry.author.gender}) -}} -
    -
    -{% elseif entry.type=="JOIN_GROUP" %} -
    -
    -
    {{ formatTime(entry.time) }}
    -
    - {{ entry.author.fullName }} - {{- L('feed_joined_group_before', {'gender': entry.author.gender}) -}} - {{ entry.group.name }} - {{- L('feed_joined_group_after', {'gender': entry.author.gender}) -}} -
    -
    -{%else%} -Unknown entry type {{entry.type}} -{%endif%} +{% include "feed_row" with {'entry': entry} %} {% else %}
    {{ L('feed_empty') }}
    {%endfor%} diff --git a/src/main/resources/templates/mobile/feed.twig b/src/main/resources/templates/mobile/feed.twig index 7c72a002..adf629e6 100644 --- a/src/main/resources/templates/mobile/feed.twig +++ b/src/main/resources/templates/mobile/feed.twig @@ -8,44 +8,7 @@
    {%for entry in feed%}
    -{%if entry.type=="POST"%} -{%include "wall_post" with {'post': entry.post}%} -{%elseif entry.type=="RETOOT"%} -
    -
    -
    {{ formatTime(entry.time) }}
    -
    - {{ L(entry.post.replyLevel==0 ? 'feed_retoot_before' : 'feed_retoot_before_comment', {'gender': entry.author.gender}) -}} - {{entry.author.fullName}} - {{- L(entry.post.replyLevel==0 ? 'feed_retoot_after' : 'feed_retoot_after_comment', {'gender': entry.author.gender}) -}} -
    -
    -{%include "wall_post" with {'post': entry.post}%} -{% elseif entry.type=="ADD_FRIEND" %} -
    -
    -
    {{ formatTime(entry.time) }}
    -
    - {{ entry.author.fullName }} - {{- L('feed_added_friend_before', {'gender': entry.author.gender}) -}} - {{ L('feed_added_friend_name', {'name': entry.friend.firstLastAndGender}) }} - {{- L('feed_added_friend_after', {'gender': entry.author.gender}) -}} -
    -
    -{% elseif entry.type=="JOIN_GROUP" %} -
    -
    -
    {{ formatTime(entry.time) }}
    -
    - {{ entry.author.fullName }} - {{- L('feed_joined_group_before', {'gender': entry.author.gender}) -}} - {{ entry.group.name }} - {{- L('feed_joined_group_after', {'gender': entry.author.gender}) -}} -
    -
    -{%else%} -Unknown entry type {{entry.type}} -{%endif%} + {% include "feed_row" with {'entry': entry} %}
    {% else %}
    {{ L('feed_empty') }}
    diff --git a/src/main/web/common.scss b/src/main/web/common.scss index 47036382..de9b737b 100644 --- a/src/main/web/common.scss +++ b/src/main/web/common.scss @@ -712,6 +712,9 @@ ul.actualList{ &.feedIconStatus{ background: svg-load('img/statuses_s.svg'); } + &.feedIconEvent{ + background: svg-load('img/events_s.svg'); + } } .undecoratedLink{ diff --git a/src/main/web/package-lock.json b/src/main/web/package-lock.json index 020259b4..18aec6ce 100644 --- a/src/main/web/package-lock.json +++ b/src/main/web/package-lock.json @@ -1,6 +1,2462 @@ { + "name": "web", + "lockfileVersion": 2, "requires": true, - "lockfileVersion": 1, + "packages": { + "": { + "dependencies": { + "autoprefixer": "latest", + "cssnano": "latest", + "postcss": "^8.4.4", + "postcss-advanced-variables": "latest", + "postcss-cli": "latest", + "postcss-color-function": "latest", + "postcss-import": "latest", + "postcss-inline-svg": "latest", + "postcss-nested": "latest", + "postcss-rgba-hex": "latest", + "typescript": "^4.5.3", + "uglify-js": "latest" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dependencies": { + "@babel/highlight": "^7.12.13" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==" + }, + "node_modules/@babel/highlight": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "node_modules/@csstools/sass-import-resolve": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/sass-import-resolve/-/sass-import-resolve-1.0.0.tgz", + "integrity": "sha512-pH4KCsbtBLLe7eqUrw8brcuFO8IZlN36JjdKlOublibVdAIPHCzEnpBWOVUXK5sCf+DpBi8ZtuWtjF0srybdeA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", + "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", + "dependencies": { + "@nodelib/fs.stat": "2.0.4", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", + "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.4", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@trysound/sax": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.1.1.tgz", + "integrity": "sha512-Z6DoceYb/1xSg5+e+ZlPZ9v0N16ZvZ+wYMraFue4HYrE4ttONKtsvruIRf6t9TBR0YvSOfi1hUU0fJfBLCDYow==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + }, + "node_modules/alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=" + }, + "node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.5.tgz", + "integrity": "sha512-7H4AJZXvSsn62SqZyJCP+1AWwOuoYpUfK6ot9vm0e87XD6mT8lDywc9D9OTJPMULyGcvmIxzTAMeG2Cc+YX+fA==", + "dependencies": { + "browserslist": "^4.16.3", + "caniuse-lite": "^1.0.30001196", + "colorette": "^1.2.2", + "fraction.js": "^4.0.13", + "normalize-range": "^0.1.2", + "postcss-value-parser": "^4.1.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.1.0.tgz", + "integrity": "sha1-tQS9BYabOSWd0MXvw12EMXbczEo=" + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", + "dependencies": { + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001286", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001286.tgz", + "integrity": "sha512-zaEMRH6xg8ESMi2eQ3R4eZ5qw/hJiVsO/HlLwniIwErij0JDr9P+8V4dtx1l+kLq6j3yy8l8W4fst1lBnat5wQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "dependencies": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.1" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", + "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=", + "dependencies": { + "clone": "^1.0.2", + "color-convert": "^1.3.0", + "color-string": "^0.3.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/color-string": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", + "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", + "dependencies": { + "color-name": "^1.0.0" + } + }, + "node_modules/colord": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.0.0.tgz", + "integrity": "sha512-WMDFJfoY3wqPZNpKUFdse3HhD5BHCbE9JCdxRzoVH+ywRITGOeWAHNkGEmyxLlErEpN9OLMWgdM9dWQtDk5dog==" + }, + "node_modules/colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cosmiconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.0.tgz", + "integrity": "sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-color-function": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/css-color-function/-/css-color-function-1.3.3.tgz", + "integrity": "sha1-jtJMLAIFBzM5+voAS8jBQfzLKC4=", + "dependencies": { + "balanced-match": "0.1.0", + "color": "^0.11.0", + "debug": "^3.1.0", + "rgb": "~0.1.0" + } + }, + "node_modules/css-color-names": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-1.0.1.tgz", + "integrity": "sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==", + "engines": { + "node": "*" + } + }, + "node_modules/css-declaration-sorter": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.0.3.tgz", + "integrity": "sha512-52P95mvW1SMzuRZegvpluT6yEv0FqQusydKQPZsNN5Q7hh8EwQvN8E2nwuJ16BBvNN6LcoIZXu/Bk58DAhrrxw==", + "dependencies": { + "timsort": "^0.3.0" + }, + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-select": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-3.1.2.tgz", + "integrity": "sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^4.0.0", + "domhandler": "^4.0.0", + "domutils": "^2.4.3", + "nth-check": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-what": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-4.0.0.tgz", + "integrity": "sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.4.tgz", + "integrity": "sha512-I+fDW74CJ4yb31765ov9xXe70XLZvFTXjwhmA//VgAAuSAU34Oblbe94Q9zffiCX1VhcSfQWARQnwhz+Nqgb4Q==", + "dependencies": { + "cosmiconfig": "^7.0.0", + "cssnano-preset-default": "^5.1.1", + "is-resolvable": "^1.1.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-preset-default": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.1.tgz", + "integrity": "sha512-kAhR71Tascmnjlhl4UegGA3KGGbMLXHkkqVpA9idsRT1JmIhIsz1C3tDpBeQMUw5EX5Rfb1HGc/PRqD2AFk3Vg==", + "dependencies": { + "css-declaration-sorter": "^6.0.3", + "cssnano-utils": "^2.0.1", + "postcss-calc": "^8.0.0", + "postcss-colormin": "^5.1.1", + "postcss-convert-values": "^5.0.1", + "postcss-discard-comments": "^5.0.1", + "postcss-discard-duplicates": "^5.0.1", + "postcss-discard-empty": "^5.0.1", + "postcss-discard-overridden": "^5.0.1", + "postcss-merge-longhand": "^5.0.2", + "postcss-merge-rules": "^5.0.1", + "postcss-minify-font-values": "^5.0.1", + "postcss-minify-gradients": "^5.0.1", + "postcss-minify-params": "^5.0.1", + "postcss-minify-selectors": "^5.1.0", + "postcss-normalize-charset": "^5.0.1", + "postcss-normalize-display-values": "^5.0.1", + "postcss-normalize-positions": "^5.0.1", + "postcss-normalize-repeat-style": "^5.0.1", + "postcss-normalize-string": "^5.0.1", + "postcss-normalize-timing-functions": "^5.0.1", + "postcss-normalize-unicode": "^5.0.1", + "postcss-normalize-url": "^5.0.1", + "postcss-normalize-whitespace": "^5.0.1", + "postcss-ordered-values": "^5.0.1", + "postcss-reduce-initial": "^5.0.1", + "postcss-reduce-transforms": "^5.0.1", + "postcss-svgo": "^5.0.1", + "postcss-unique-selectors": "^5.0.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz", + "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/dependency-graph": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.9.0.tgz", + "integrity": "sha512-9YLIBURXj4DJMFALxXw9K3Y3rwb5Fk0X5/8ipCzaN84+gKxoHK43tVKRNakCQbiEx07E8Uwhuq21BpUagFhZ8w==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", + "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.6.0.tgz", + "integrity": "sha512-y0BezHuy4MDYxh6OvolXYsH+1EMGmFbwv5FKW7ovwMG6zTPWqNPq3WF9ayZssFq+UlKdffGLbOEaghNdaOm1WA==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.3.737", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.737.tgz", + "integrity": "sha512-P/B84AgUSQXaum7a8m11HUsYL8tj9h/Pt5f7Hg7Ty6bm5DxlFq+e5+ouHUoNQMsKDJ7u4yGfI8mOErCmSH9wyg==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fast-glob": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", + "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fastq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", + "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.1.tgz", + "integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", + "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==" + }, + "node_modules/hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=" + }, + "node_modules/hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=" + }, + "node_modules/htmlparser2": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", + "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^3.3.0", + "domutils": "^2.4.2", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/fb55/htmlparser2?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "dependencies": { + "domelementtype": "^2.0.1" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", + "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", + "dependencies": { + "import-from": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-from/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-absolute-url": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", + "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dependencies": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, + "node_modules/is-color-stop/node_modules/css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "engines": { + "node": "*" + } + }, + "node_modules/is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==" + }, + "node_modules/js-base64": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", + "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" + }, + "node_modules/lodash.forown": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.forown/-/lodash.forown-4.4.0.tgz", + "integrity": "sha1-hRFc8E9z75ZuztUlEdOJPMRmg68=" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha1-Cwih3PaDl8OXhVwyOXg4Mt90A9E=" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" + }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "3.1.30", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", + "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "1.1.72", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz", + "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz", + "integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "8.4.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.4.tgz", + "integrity": "sha512-joU6fBsN6EIer28Lj6GDFoC/5yOZzLCfn0zHAn/MYXI7aPt4m4hK5KC5ovEZXy+lnCjmYIbQWngvju2ddyEr8Q==", + "dependencies": { + "nanoid": "^3.1.30", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-advanced-variables": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postcss-advanced-variables/-/postcss-advanced-variables-3.0.1.tgz", + "integrity": "sha512-JqVjfkmqPoazMobVeQYzbt7djcDGJfMlpwBd9abTqmzWR40tvIUMXpTU5w3riqz7h+wYPY7V6GF8BIXL/ybEfg==", + "dependencies": { + "@csstools/sass-import-resolve": "^1.0.0", + "postcss": "^7.0.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-advanced-variables/node_modules/postcss": { + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", + "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "dependencies": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-advanced-variables/node_modules/supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/postcss-calc": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.0.0.tgz", + "integrity": "sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g==", + "dependencies": { + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.2" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-cli": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/postcss-cli/-/postcss-cli-8.3.1.tgz", + "integrity": "sha512-leHXsQRq89S3JC9zw/tKyiVV2jAhnfQe0J8VI4eQQbUjwIe0XxVqLrR+7UsahF1s9wi4GlqP6SJ8ydf44cgF2Q==", + "dependencies": { + "chalk": "^4.0.0", + "chokidar": "^3.3.0", + "dependency-graph": "^0.9.0", + "fs-extra": "^9.0.0", + "get-stdin": "^8.0.0", + "globby": "^11.0.0", + "postcss-load-config": "^3.0.0", + "postcss-reporter": "^7.0.0", + "pretty-hrtime": "^1.0.3", + "read-cache": "^1.0.0", + "slash": "^3.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "postcss": "bin/postcss" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/postcss-cli/node_modules/chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/postcss-cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/postcss-cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/postcss-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss-color-function": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-color-function/-/postcss-color-function-4.1.0.tgz", + "integrity": "sha512-2/fuv6mP5Lt03XbRpVfMdGC8lRP1sykme+H1bR4ARyOmSMB8LPSjcL6EAI1iX6dqUF+jNEvKIVVXhan1w/oFDQ==", + "dependencies": { + "css-color-function": "~1.3.3", + "postcss": "^6.0.23", + "postcss-message-helpers": "^2.0.0", + "postcss-value-parser": "^3.3.1" + } + }, + "node_modules/postcss-color-function/node_modules/postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", + "dependencies": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/postcss-color-function/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + }, + "node_modules/postcss-colormin": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.1.1.tgz", + "integrity": "sha512-SyTmqKKN6PyYNeeKEC0hqIP5CDuprO1hHurdW1aezDyfofDUOn7y7MaxcolbsW3oazPwFiGiY30XRiW1V4iZpA==", + "dependencies": { + "browserslist": "^4.16.0", + "colord": "^2.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-convert-values": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.0.1.tgz", + "integrity": "sha512-C3zR1Do2BkKkCgC0g3sF8TS0koF2G+mN8xxayZx3f10cIRmTaAnpgpRQZjNekTZxM2ciSPoh2IWJm0VZx8NoQg==", + "dependencies": { + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-comments": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz", + "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz", + "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-empty": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz", + "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz", + "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-import": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.0.2.tgz", + "integrity": "sha512-BJ2pVK4KhUyMcqjuKs9RijV5tatNzNa73e/32aBVE/ejYPe37iH+6vAu9WvqUkB5OAYgLHzbSvzHnorybJCm9g==", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-inline-svg": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-inline-svg/-/postcss-inline-svg-5.0.0.tgz", + "integrity": "sha512-Agqkrn91Qgi+KAO+cTvUS1IAZbHPD4sryPoG0q5U0ThokL4UGoMcmwvNV6tDoRp69B5tgD1VNkn9P09E+xpQAg==", + "dependencies": { + "css-select": "^3.1.0", + "dom-serializer": "^1.1.0", + "htmlparser2": "^5.0.1", + "postcss-value-parser": "^4.0.0" + }, + "peerDependencies": { + "postcss": "^8.1.4" + } + }, + "node_modules/postcss-load-config": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.0.1.tgz", + "integrity": "sha512-/pDHe30UYZUD11IeG8GWx9lNtu1ToyTsZHnyy45B4Mrwr/Kb6NgYl7k753+05CJNKnjbwh4975amoPJ+TEjHNQ==", + "dependencies": { + "cosmiconfig": "^7.0.0", + "import-cwd": "^3.0.0" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.0.2.tgz", + "integrity": "sha512-BMlg9AXSI5G9TBT0Lo/H3PfUy63P84rVz3BjCFE9e9Y9RXQZD3+h3YO1kgTNsNJy7bBc1YQp8DmSnwLIW5VPcw==", + "dependencies": { + "css-color-names": "^1.0.1", + "postcss-value-parser": "^4.1.0", + "stylehacks": "^5.0.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-rules": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.0.1.tgz", + "integrity": "sha512-UR6R5Ph0c96QB9TMBH3ml8/kvPCThPHepdhRqAbvMRDRHQACPC8iM5NpfIC03+VRMZTGXy4L/BvFzcDFCgb+fA==", + "dependencies": { + "browserslist": "^4.16.0", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^2.0.1", + "postcss-selector-parser": "^6.0.5", + "vendors": "^1.0.3" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-message-helpers": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz", + "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=" + }, + "node_modules/postcss-minify-font-values": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz", + "integrity": "sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA==", + "dependencies": { + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.0.1.tgz", + "integrity": "sha512-odOwBFAIn2wIv+XYRpoN2hUV3pPQlgbJ10XeXPq8UY2N+9ZG42xu45lTn/g9zZ+d70NKSQD6EOi6UiCMu3FN7g==", + "dependencies": { + "cssnano-utils": "^2.0.1", + "is-color-stop": "^1.1.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-params": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz", + "integrity": "sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw==", + "dependencies": { + "alphanum-sort": "^1.0.2", + "browserslist": "^4.16.0", + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0", + "uniqs": "^2.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz", + "integrity": "sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og==", + "dependencies": { + "alphanum-sort": "^1.0.2", + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-nested": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.5.tgz", + "integrity": "sha512-GSRXYz5bccobpTzLQZXOnSOfKl6TwVr5CyAQJUPub4nuRJSOECK5AqurxVgmtxP48p0Kc/ndY/YyS1yqldX0Ew==", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.1.13" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz", + "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz", + "integrity": "sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ==", + "dependencies": { + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz", + "integrity": "sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg==", + "dependencies": { + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz", + "integrity": "sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w==", + "dependencies": { + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-string": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz", + "integrity": "sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA==", + "dependencies": { + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz", + "integrity": "sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q==", + "dependencies": { + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz", + "integrity": "sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA==", + "dependencies": { + "browserslist": "^4.16.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-url": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.1.tgz", + "integrity": "sha512-hkbG0j58Z1M830/CJ73VsP7gvlG1yF+4y7Fd1w4tD2c7CaA2Psll+pQ6eQhth9y9EaqZSLzamff/D0MZBMbYSg==", + "dependencies": { + "is-absolute-url": "^3.0.3", + "normalize-url": "^4.5.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz", + "integrity": "sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA==", + "dependencies": { + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-ordered-values": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.0.1.tgz", + "integrity": "sha512-6mkCF5BQ25HvEcDfrMHCLLFHlraBSlOXFnQMHYhSpDO/5jSR1k8LdEXOkv+7+uzW6o6tBYea1Km0wQSRkPJkwA==", + "dependencies": { + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz", + "integrity": "sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw==", + "dependencies": { + "browserslist": "^4.16.0", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz", + "integrity": "sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA==", + "dependencies": { + "cssnano-utils": "^2.0.1", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reporter": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-7.0.2.tgz", + "integrity": "sha512-JyQ96NTQQsso42y6L1H1RqHfWH1C3Jr0pt91mVv5IdYddZAE9DUZxuferNgk6q0o6vBVOrfVJb10X1FgDzjmDw==", + "dependencies": { + "colorette": "^1.2.1", + "lodash.difference": "^4.5.0", + "lodash.forown": "^4.4.0", + "lodash.get": "^4.4.2", + "lodash.groupby": "^4.6.0", + "lodash.sortby": "^4.7.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-rgba-hex": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/postcss-rgba-hex/-/postcss-rgba-hex-0.3.7.tgz", + "integrity": "sha1-QQkwf3MxesreVtLVvsvU+qFbgKw=", + "dependencies": { + "object-assign": "^4.1.0", + "postcss": "^5.0.10", + "rgb2hex": "^0.1.0" + } + }, + "node_modules/postcss-rgba-hex/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss-rgba-hex/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss-rgba-hex/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss-rgba-hex/node_modules/chalk/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/postcss-rgba-hex/node_modules/has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss-rgba-hex/node_modules/postcss": { + "version": "5.2.18", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", + "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", + "dependencies": { + "chalk": "^1.1.3", + "js-base64": "^2.1.9", + "source-map": "^0.5.6", + "supports-color": "^3.2.3" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/postcss-rgba-hex/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss-rgba-hex/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss-rgba-hex/node_modules/supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dependencies": { + "has-flag": "^1.0.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", + "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.0.1.tgz", + "integrity": "sha512-cD7DFo6tF9i5eWvwtI4irKOHCpmASFS0xvZ5EQIgEdA1AWfM/XiHHY/iss0gcKHhkqwgYmuo2M0KhJLd5Us6mg==", + "dependencies": { + "postcss-value-parser": "^4.1.0", + "svgo": "^2.3.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz", + "integrity": "sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w==", + "dependencies": { + "alphanum-sort": "^1.0.2", + "postcss-selector-parser": "^6.0.5", + "uniqs": "^2.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" + }, + "node_modules/pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rgb": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/rgb/-/rgb-0.1.0.tgz", + "integrity": "sha1-vieykej+/+rBvZlylyG/pA/AN7U=", + "bin": { + "rgb": "bin/rgb" + } + }, + "node_modules/rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=" + }, + "node_modules/rgb2hex": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.1.10.tgz", + "integrity": "sha512-vKz+kzolWbL3rke/xeTE2+6vHmZnNxGyDnaVW4OckntAIcc7DcZzWkQSfxMDwqHS8vhgySnIFyBUH7lIk6PxvQ==" + }, + "node_modules/rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz", + "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==" + }, + "node_modules/string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylehacks": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.0.1.tgz", + "integrity": "sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA==", + "dependencies": { + "browserslist": "^4.16.0", + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/svgo": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.3.0.tgz", + "integrity": "sha512-fz4IKjNO6HDPgIQxu4IxwtubtbSfGEAJUq/IXyTPIkGhWck/faiiwfkvsB8LnBkKLvSoyNNIY6d13lZprJMc9Q==", + "dependencies": { + "@trysound/sax": "0.1.1", + "chalk": "^4.1.0", + "commander": "^7.1.0", + "css-select": "^3.1.2", + "css-tree": "^1.1.2", + "csso": "^4.2.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/svgo/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/svgo/node_modules/chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/svgo/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/svgo/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/svgo/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/svgo/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/typescript": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.3.tgz", + "integrity": "sha512-eVYaEHALSt+s9LbvgEv4Ef+Tdq7hBiIZgii12xXJnukryt3pMgJf6aKhoCZ3FWQsu6sydEnkg11fYXLzhLBjeQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uglify-js": { + "version": "3.13.7", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.7.tgz", + "integrity": "sha512-1Psi2MmnZJbnEsgJJIlfnd7tFlJfitusmR7zDI8lXlFI0ACD4/Rm/xdrU8bh6zF0i74aiVoBtkRiFulkrmh3AA==", + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=" + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/vendors": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", + "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "engines": { + "node": ">=10" + } + } + }, "dependencies": { "@babel/code-frame": { "version": "7.12.13", @@ -371,7 +2827,8 @@ "cssnano-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz", - "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==" + "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==", + "requires": {} }, "csso": { "version": "4.2.0", @@ -1066,22 +3523,26 @@ "postcss-discard-comments": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz", - "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==" + "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==", + "requires": {} }, "postcss-discard-duplicates": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz", - "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==" + "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==", + "requires": {} }, "postcss-discard-empty": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz", - "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==" + "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==", + "requires": {} }, "postcss-discard-overridden": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz", - "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==" + "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==", + "requires": {} }, "postcss-import": { "version": "14.0.2", @@ -1190,7 +3651,8 @@ "postcss-normalize-charset": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz", - "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==" + "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==", + "requires": {} }, "postcss-normalize-display-values": { "version": "5.0.1", From 14850a5b7765fc9282fae887dfde08bba9c01985 Mon Sep 17 00:00:00 2001 From: Grishka Date: Mon, 20 Dec 2021 21:39:00 +0300 Subject: [PATCH 14/65] Fix posting images --- src/main/java/smithereen/controllers/WallController.java | 4 ++-- src/main/java/smithereen/routes/PostRoutes.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/smithereen/controllers/WallController.java b/src/main/java/smithereen/controllers/WallController.java index 5a144ac2..46b863f1 100644 --- a/src/main/java/smithereen/controllers/WallController.java +++ b/src/main/java/smithereen/controllers/WallController.java @@ -73,7 +73,7 @@ public WallController(ApplicationContext context){ * @param poll Poll to attach. * @return The newly created post. */ - public Post createWallPost(@NotNull User author, @NotNull Actor wallOwner, int inReplyToID, + public Post createWallPost(@NotNull User author, int authorAccountID, @NotNull Actor wallOwner, int inReplyToID, @NotNull String textSource, @Nullable String contentWarning, @NotNull List attachmentIDs, @Nullable Poll poll){ try{ @@ -111,7 +111,7 @@ public Post createWallPost(@NotNull User author, @NotNull Actor wallOwner, int i for(String id:attachmentIDs){ if(!id.matches("^[a-fA-F0-9]{32}$")) continue; - ActivityPubObject obj=MediaCache.getAndDeleteDraftAttachment(id, author.id); + ActivityPubObject obj=MediaCache.getAndDeleteDraftAttachment(id, authorAccountID); if(obj!=null){ attachObjects.add(obj); attachmentCount++; diff --git a/src/main/java/smithereen/routes/PostRoutes.java b/src/main/java/smithereen/routes/PostRoutes.java index 2eaf88fe..e8435409 100644 --- a/src/main/java/smithereen/routes/PostRoutes.java +++ b/src/main/java/smithereen/routes/PostRoutes.java @@ -114,7 +114,7 @@ public static Object createWallPost(Request req, Response resp, Account self, @N else attachments=Collections.emptyList(); - Post post=context(req).getWallController().createWallPost(self.user, owner, replyTo, text, contentWarning, attachments, poll); + Post post=context(req).getWallController().createWallPost(self.user, self.id, owner, replyTo, text, contentWarning, attachments, poll); SessionInfo sess=sessionInfo(req); sess.postDraftAttachments.clear(); From f6c639d9e5c3d9b218f785584c9cd14022e244fb Mon Sep 17 00:00:00 2001 From: Grishka Date: Mon, 20 Dec 2021 23:47:41 +0300 Subject: [PATCH 15/65] Tentative membership in events --- schema.sql | 414 ++++++++---------- src/main/java/smithereen/Mailer.java | 2 +- .../smithereen/SmithereenApplication.java | 9 +- src/main/java/smithereen/Utils.java | 23 + .../activitypub/ActivityPubWorker.java | 40 +- .../handlers/FollowGroupHandler.java | 8 +- .../objects/ActivityPubObject.java | 3 +- .../smithereen/activitypub/objects/Actor.java | 2 +- .../activitypub/objects/activities/Join.java | 20 +- .../controllers/GroupsController.java | 74 +++- .../java/smithereen/data/ForeignGroup.java | 32 ++ src/main/java/smithereen/data/Group.java | 23 +- .../exceptions/UserErrorException.java | 18 + .../java/smithereen/jsonld/JLDProcessor.java | 7 + .../smithereen/routes/ActivityPubRoutes.java | 24 +- .../java/smithereen/routes/GroupsRoutes.java | 62 ++- .../java/smithereen/routes/ProfileRoutes.java | 10 +- .../storage/DatabaseSchemaUpdater.java | 4 +- .../smithereen/storage/DatabaseUtils.java | 16 + .../java/smithereen/storage/GroupStorage.java | 56 ++- src/main/resources/langs/en.json | 11 +- src/main/resources/langs/ru.json | 13 +- .../resources/templates/desktop/group.twig | 23 +- .../resources/templates/mobile/group.twig | 21 +- src/main/web/mobile.scss | 4 +- 25 files changed, 577 insertions(+), 342 deletions(-) create mode 100644 src/main/java/smithereen/exceptions/UserErrorException.java diff --git a/schema.sql b/schema.sql index 43feae43..1ef92883 100644 --- a/schema.sql +++ b/schema.sql @@ -1,37 +1,22 @@ -# ************************************************************ -# Sequel Ace SQL dump -# Версия 3043 -# -# https://sequel-ace.com/ -# https://github.com/Sequel-Ace/Sequel-Ace -# -# Хост: 127.0.0.1 (MySQL 5.7.9) -# База данных: smithereen -# Generation Time: 2021-11-20 11:18:32 +0000 -# ************************************************************ - - -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -SET NAMES utf8mb4; -/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE='NO_AUTO_VALUE_ON_ZERO', SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; -/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; - - -# Dump of table accounts -# ------------------------------------------------------------ +-- MySQL dump 10.13 Distrib 8.0.27, for macos12.0 (arm64) +-- +-- Host: localhost Database: smithereen +-- ------------------------------------------------------ +-- Server version 8.0.27 + +-- +-- Table structure for table `accounts` +-- CREATE TABLE `accounts` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `user_id` int(11) unsigned NOT NULL, + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, `email` varchar(200) NOT NULL DEFAULT '', `password` binary(32) DEFAULT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `invited_by` int(11) unsigned DEFAULT NULL, - `access_level` tinyint(3) unsigned NOT NULL DEFAULT '1', - `preferences` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin, + `invited_by` int unsigned DEFAULT NULL, + `access_level` tinyint unsigned NOT NULL DEFAULT '1', + `preferences` text, `last_active` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `ban_info` text, PRIMARY KEY (`id`), @@ -39,79 +24,73 @@ CREATE TABLE `accounts` ( CONSTRAINT `accounts_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table blocks_group_domain -# ------------------------------------------------------------ +-- +-- Table structure for table `blocks_group_domain` +-- CREATE TABLE `blocks_group_domain` ( - `owner_id` int(10) unsigned NOT NULL, - `domain` varchar(100) CHARACTER SET ascii NOT NULL, + `owner_id` int unsigned NOT NULL, + `domain` varchar(100) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, UNIQUE KEY `owner_id` (`owner_id`,`domain`), KEY `domain` (`domain`), CONSTRAINT `blocks_group_domain_ibfk_1` FOREIGN KEY (`owner_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table blocks_group_user -# ------------------------------------------------------------ +-- +-- Table structure for table `blocks_group_user` +-- CREATE TABLE `blocks_group_user` ( - `owner_id` int(10) unsigned NOT NULL, - `user_id` int(10) unsigned NOT NULL, + `owner_id` int unsigned NOT NULL, + `user_id` int unsigned NOT NULL, UNIQUE KEY `owner_id` (`owner_id`,`user_id`), KEY `user_id` (`user_id`), CONSTRAINT `blocks_group_user_ibfk_1` FOREIGN KEY (`owner_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `blocks_group_user_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table blocks_user_domain -# ------------------------------------------------------------ +-- +-- Table structure for table `blocks_user_domain` +-- CREATE TABLE `blocks_user_domain` ( - `owner_id` int(10) unsigned NOT NULL, - `domain` varchar(100) CHARACTER SET ascii NOT NULL, + `owner_id` int unsigned NOT NULL, + `domain` varchar(100) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, UNIQUE KEY `owner_id` (`owner_id`,`domain`), KEY `domain` (`domain`), CONSTRAINT `blocks_user_domain_ibfk_1` FOREIGN KEY (`owner_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table blocks_user_user -# ------------------------------------------------------------ +-- +-- Table structure for table `blocks_user_user` +-- CREATE TABLE `blocks_user_user` ( - `owner_id` int(10) unsigned NOT NULL, - `user_id` int(10) unsigned NOT NULL, + `owner_id` int unsigned NOT NULL, + `user_id` int unsigned NOT NULL, UNIQUE KEY `owner_id` (`owner_id`,`user_id`), KEY `user_id` (`user_id`), CONSTRAINT `blocks_user_user_ibfk_1` FOREIGN KEY (`owner_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, CONSTRAINT `blocks_user_user_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table config -# ------------------------------------------------------------ +-- +-- Table structure for table `config` +-- CREATE TABLE `config` ( - `key` varchar(255) CHARACTER SET ascii NOT NULL DEFAULT '', + `key` varchar(255) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL DEFAULT '', `value` text NOT NULL, PRIMARY KEY (`key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table draft_attachments -# ------------------------------------------------------------ +-- +-- Table structure for table `draft_attachments` +-- CREATE TABLE `draft_attachments` ( `id` binary(16) NOT NULL, - `owner_account_id` int(10) unsigned NOT NULL, + `owner_account_id` int unsigned NOT NULL, `info` text NOT NULL, `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), @@ -119,15 +98,14 @@ CREATE TABLE `draft_attachments` ( CONSTRAINT `draft_attachments_ibfk_1` FOREIGN KEY (`owner_account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table email_codes -# ------------------------------------------------------------ +-- +-- Table structure for table `email_codes` +-- CREATE TABLE `email_codes` ( `code` binary(64) NOT NULL, - `account_id` int(10) unsigned DEFAULT NULL, - `type` int(11) NOT NULL, + `account_id` int unsigned DEFAULT NULL, + `type` int NOT NULL, `extra` text, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`code`), @@ -135,14 +113,13 @@ CREATE TABLE `email_codes` ( CONSTRAINT `email_codes_ibfk_1` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table followings -# ------------------------------------------------------------ +-- +-- Table structure for table `followings` +-- CREATE TABLE `followings` ( - `follower_id` int(11) unsigned NOT NULL, - `followee_id` int(11) unsigned NOT NULL, + `follower_id` int unsigned NOT NULL, + `followee_id` int unsigned NOT NULL, `mutual` tinyint(1) NOT NULL DEFAULT '0', `accepted` tinyint(1) NOT NULL DEFAULT '1', KEY `follower_id` (`follower_id`), @@ -153,15 +130,14 @@ CREATE TABLE `followings` ( CONSTRAINT `followings_ibfk_2` FOREIGN KEY (`followee_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table friend_requests -# ------------------------------------------------------------ +-- +-- Table structure for table `friend_requests` +-- CREATE TABLE `friend_requests` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `from_user_id` int(11) unsigned NOT NULL, - `to_user_id` int(11) unsigned NOT NULL, + `id` int unsigned NOT NULL AUTO_INCREMENT, + `from_user_id` int unsigned NOT NULL, + `to_user_id` int unsigned NOT NULL, `message` text, PRIMARY KEY (`id`), UNIQUE KEY `from_user_id` (`from_user_id`,`to_user_id`), @@ -170,32 +146,30 @@ CREATE TABLE `friend_requests` ( CONSTRAINT `friend_requests_ibfk_2` FOREIGN KEY (`to_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table group_admins -# ------------------------------------------------------------ +-- +-- Table structure for table `group_admins` +-- CREATE TABLE `group_admins` ( - `user_id` int(11) unsigned NOT NULL, - `group_id` int(11) unsigned NOT NULL, - `level` int(11) unsigned NOT NULL, + `user_id` int unsigned NOT NULL, + `group_id` int unsigned NOT NULL, + `level` int unsigned NOT NULL, `title` varchar(300) DEFAULT NULL, - `display_order` int(10) unsigned NOT NULL DEFAULT '0', + `display_order` int unsigned NOT NULL DEFAULT '0', KEY `user_id` (`user_id`), KEY `group_id` (`group_id`), CONSTRAINT `group_admins_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, CONSTRAINT `group_admins_ibfk_2` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table group_memberships -# ------------------------------------------------------------ +-- +-- Table structure for table `group_memberships` +-- CREATE TABLE `group_memberships` ( - `user_id` int(11) unsigned NOT NULL, - `group_id` int(11) unsigned NOT NULL, - `post_feed_visibility` tinyint(4) unsigned NOT NULL DEFAULT '0', + `user_id` int unsigned NOT NULL, + `group_id` int unsigned NOT NULL, + `post_feed_visibility` tinyint unsigned NOT NULL DEFAULT '0', `tentative` tinyint(1) NOT NULL DEFAULT '0', `accepted` tinyint(1) NOT NULL DEFAULT '1', UNIQUE KEY `user_id` (`user_id`,`group_id`), @@ -204,17 +178,16 @@ CREATE TABLE `group_memberships` ( CONSTRAINT `group_memberships_ibfk_2` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table groups -# ------------------------------------------------------------ +-- +-- Table structure for table `groups` +-- CREATE TABLE `groups` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `id` int unsigned NOT NULL AUTO_INCREMENT, `name` varchar(200) NOT NULL DEFAULT '', `username` varchar(50) NOT NULL DEFAULT '', `domain` varchar(100) NOT NULL DEFAULT '', - `ap_id` varchar(300) CHARACTER SET ascii DEFAULT NULL, + `ap_id` varchar(300) CHARACTER SET ascii COLLATE ascii_general_ci DEFAULT NULL, `ap_url` varchar(300) DEFAULT NULL, `ap_inbox` varchar(300) DEFAULT NULL, `ap_shared_inbox` varchar(300) DEFAULT NULL, @@ -227,28 +200,28 @@ CREATE TABLE `groups` ( `profile_fields` text, `event_start_time` timestamp NULL DEFAULT NULL, `event_end_time` timestamp NULL DEFAULT NULL, - `type` tinyint(3) unsigned NOT NULL DEFAULT '0', - `member_count` int(10) unsigned NOT NULL DEFAULT '0', - `tentative_member_count` int(10) unsigned NOT NULL DEFAULT '0', + `type` tinyint unsigned NOT NULL DEFAULT '0', + `member_count` int unsigned NOT NULL DEFAULT '0', + `tentative_member_count` int unsigned NOT NULL DEFAULT '0', `ap_followers` varchar(300) DEFAULT NULL, `ap_wall` varchar(300) DEFAULT NULL, `last_updated` timestamp NULL DEFAULT NULL, + `flags` bigint unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`,`domain`), UNIQUE KEY `ap_id` (`ap_id`), KEY `type` (`type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table likes -# ------------------------------------------------------------ +-- +-- Table structure for table `likes` +-- CREATE TABLE `likes` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `user_id` int(11) unsigned NOT NULL, - `object_id` int(11) unsigned NOT NULL, - `object_type` int(11) unsigned NOT NULL, + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, + `object_id` int unsigned NOT NULL, + `object_type` int unsigned NOT NULL, `ap_id` varchar(300) DEFAULT NULL, UNIQUE KEY `user_id` (`user_id`,`object_id`,`object_type`), UNIQUE KEY `id` (`id`), @@ -256,31 +229,29 @@ CREATE TABLE `likes` ( CONSTRAINT `likes_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table media_cache -# ------------------------------------------------------------ +-- +-- Table structure for table `media_cache` +-- CREATE TABLE `media_cache` ( `url_hash` binary(16) NOT NULL, - `size` int(11) unsigned NOT NULL, + `size` int unsigned NOT NULL, `last_access` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `info` blob, - `type` tinyint(4) NOT NULL, + `type` tinyint NOT NULL, PRIMARY KEY (`url_hash`), KEY `last_access` (`last_access`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table newsfeed -# ------------------------------------------------------------ +-- +-- Table structure for table `newsfeed` +-- CREATE TABLE `newsfeed` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `type` int(11) NOT NULL, - `author_id` int(11) NOT NULL, - `object_id` int(11) DEFAULT NULL, + `id` int unsigned NOT NULL AUTO_INCREMENT, + `type` int unsigned NOT NULL, + `author_id` int NOT NULL, + `object_id` int DEFAULT NULL, `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `type` (`type`,`object_id`,`author_id`), @@ -288,15 +259,14 @@ CREATE TABLE `newsfeed` ( KEY `author_id` (`author_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table newsfeed_comments -# ------------------------------------------------------------ +-- +-- Table structure for table `newsfeed_comments` +-- CREATE TABLE `newsfeed_comments` ( - `user_id` int(10) unsigned NOT NULL, - `object_type` int(10) unsigned NOT NULL, - `object_id` int(10) unsigned NOT NULL, + `user_id` int unsigned NOT NULL, + `object_type` int unsigned NOT NULL, + `object_id` int unsigned NOT NULL, `last_comment_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`object_type`,`object_id`,`user_id`), KEY `user_id` (`user_id`), @@ -304,54 +274,51 @@ CREATE TABLE `newsfeed_comments` ( CONSTRAINT `newsfeed_comments_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table notifications -# ------------------------------------------------------------ +-- +-- Table structure for table `notifications` +-- CREATE TABLE `notifications` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `owner_id` int(11) unsigned NOT NULL, - `type` smallint(5) unsigned NOT NULL, - `object_id` int(11) unsigned DEFAULT NULL, - `object_type` smallint(5) unsigned DEFAULT NULL, - `related_object_id` int(11) unsigned DEFAULT NULL, - `related_object_type` smallint(5) unsigned DEFAULT NULL, - `actor_id` int(11) DEFAULT NULL, + `id` int unsigned NOT NULL AUTO_INCREMENT, + `owner_id` int unsigned NOT NULL, + `type` smallint unsigned NOT NULL, + `object_id` int unsigned DEFAULT NULL, + `object_type` smallint unsigned DEFAULT NULL, + `related_object_id` int unsigned DEFAULT NULL, + `related_object_type` smallint unsigned DEFAULT NULL, + `actor_id` int DEFAULT NULL, `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `owner_id` (`owner_id`), KEY `time` (`time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table poll_options -# ------------------------------------------------------------ +-- +-- Table structure for table `poll_options` +-- CREATE TABLE `poll_options` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `poll_id` int(10) unsigned NOT NULL, - `ap_id` varchar(300) CHARACTER SET ascii DEFAULT NULL, + `id` int unsigned NOT NULL AUTO_INCREMENT, + `poll_id` int unsigned NOT NULL, + `ap_id` varchar(300) CHARACTER SET ascii COLLATE ascii_general_ci DEFAULT NULL, `text` text NOT NULL, - `num_votes` int(10) unsigned NOT NULL DEFAULT '0', + `num_votes` int unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `ap_id` (`ap_id`), KEY `poll_id` (`poll_id`), CONSTRAINT `poll_options_ibfk_1` FOREIGN KEY (`poll_id`) REFERENCES `polls` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table poll_votes -# ------------------------------------------------------------ +-- +-- Table structure for table `poll_votes` +-- CREATE TABLE `poll_votes` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `user_id` int(11) unsigned NOT NULL, - `poll_id` int(10) unsigned NOT NULL, - `option_id` int(10) unsigned NOT NULL, - `ap_id` varchar(300) CHARACTER SET ascii DEFAULT NULL, + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, + `poll_id` int unsigned NOT NULL, + `option_id` int unsigned NOT NULL, + `ap_id` varchar(300) CHARACTER SET ascii COLLATE ascii_general_ci DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ap_id` (`ap_id`), KEY `user_id` (`user_id`), @@ -362,33 +329,31 @@ CREATE TABLE `poll_votes` ( CONSTRAINT `poll_votes_ibfk_3` FOREIGN KEY (`option_id`) REFERENCES `poll_options` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table polls -# ------------------------------------------------------------ +-- +-- Table structure for table `polls` +-- CREATE TABLE `polls` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `owner_id` int(10) unsigned NOT NULL, - `ap_id` varchar(300) CHARACTER SET ascii DEFAULT NULL, + `id` int unsigned NOT NULL AUTO_INCREMENT, + `owner_id` int unsigned NOT NULL, + `ap_id` varchar(300) CHARACTER SET ascii COLLATE ascii_general_ci DEFAULT NULL, `question` text, `is_anonymous` tinyint(1) NOT NULL DEFAULT '0', `is_multi_choice` tinyint(1) NOT NULL DEFAULT '0', `end_time` timestamp NULL DEFAULT NULL, - `num_voted_users` int(10) unsigned NOT NULL DEFAULT '0', + `num_voted_users` int unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `ap_id` (`ap_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table qsearch_index -# ------------------------------------------------------------ +-- +-- Table structure for table `qsearch_index` +-- CREATE TABLE `qsearch_index` ( `string` text NOT NULL, - `user_id` int(10) unsigned DEFAULT NULL, - `group_id` int(10) unsigned DEFAULT NULL, + `user_id` int unsigned DEFAULT NULL, + `group_id` int unsigned DEFAULT NULL, KEY `user_id` (`user_id`), KEY `group_id` (`group_id`), FULLTEXT KEY `string` (`string`), @@ -396,28 +361,26 @@ CREATE TABLE `qsearch_index` ( CONSTRAINT `qsearch_index_ibfk_2` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=ascii; - - -# Dump of table servers -# ------------------------------------------------------------ +-- +-- Table structure for table `servers` +-- CREATE TABLE `servers` ( `host` varchar(100) NOT NULL DEFAULT '', `software` varchar(100) DEFAULT NULL, `version` varchar(30) DEFAULT NULL, - `capabilities` bigint(20) unsigned NOT NULL DEFAULT '0', + `capabilities` bigint unsigned NOT NULL DEFAULT '0', `last_updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`host`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table sessions -# ------------------------------------------------------------ +-- +-- Table structure for table `sessions` +-- CREATE TABLE `sessions` ( `id` binary(64) NOT NULL, - `account_id` int(11) unsigned NOT NULL, + `account_id` int unsigned NOT NULL, `last_active` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `last_ip` varbinary(16) DEFAULT NULL, PRIMARY KEY (`id`), @@ -425,28 +388,26 @@ CREATE TABLE `sessions` ( CONSTRAINT `sessions_ibfk_1` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table signup_invitations -# ------------------------------------------------------------ +-- +-- Table structure for table `signup_invitations` +-- CREATE TABLE `signup_invitations` ( `code` binary(16) NOT NULL DEFAULT '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0', - `owner_id` int(11) unsigned DEFAULT NULL, + `owner_id` int unsigned DEFAULT NULL, `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `signups_remaining` int(11) unsigned NOT NULL, + `signups_remaining` int unsigned NOT NULL, PRIMARY KEY (`code`), KEY `owner_id` (`owner_id`), CONSTRAINT `signup_invitations_ibfk_1` FOREIGN KEY (`owner_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table users -# ------------------------------------------------------------ +-- +-- Table structure for table `users` +-- CREATE TABLE `users` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `id` int unsigned NOT NULL AUTO_INCREMENT, `fname` varchar(100) NOT NULL DEFAULT '', `lname` varchar(100) DEFAULT NULL, `middle_name` varchar(100) DEFAULT NULL, @@ -462,14 +423,14 @@ CREATE TABLE `users` ( `ap_shared_inbox` varchar(300) DEFAULT NULL, `about` text, `about_source` text, - `gender` tinyint(4) unsigned NOT NULL DEFAULT '0', - `profile_fields` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin, + `gender` tinyint unsigned NOT NULL DEFAULT '0', + `profile_fields` text, `avatar` text, - `ap_id` varchar(300) CHARACTER SET ascii DEFAULT NULL, + `ap_id` varchar(300) CHARACTER SET ascii COLLATE ascii_general_ci DEFAULT NULL, `ap_followers` varchar(300) DEFAULT NULL, `ap_following` varchar(300) DEFAULT NULL, `last_updated` timestamp NULL DEFAULT NULL, - `flags` bigint(20) unsigned NOT NULL, + `flags` bigint unsigned NOT NULL, `ap_wall` varchar(300) DEFAULT NULL, `ap_friends` varchar(300) DEFAULT NULL, `ap_groups` varchar(300) DEFAULT NULL, @@ -479,32 +440,31 @@ CREATE TABLE `users` ( KEY `ap_outbox` (`ap_outbox`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - -# Dump of table wall_posts -# ------------------------------------------------------------ +-- +-- Table structure for table `wall_posts` +-- CREATE TABLE `wall_posts` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `author_id` int(11) unsigned DEFAULT NULL, - `owner_user_id` int(11) unsigned DEFAULT NULL, - `owner_group_id` int(11) unsigned DEFAULT NULL, + `id` int unsigned NOT NULL AUTO_INCREMENT, + `author_id` int unsigned DEFAULT NULL, + `owner_user_id` int unsigned DEFAULT NULL, + `owner_group_id` int unsigned DEFAULT NULL, `text` text, - `attachments` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin, - `repost_of` int(11) unsigned DEFAULT NULL, + `attachments` text, + `repost_of` int unsigned DEFAULT NULL, `ap_url` varchar(300) DEFAULT NULL, - `ap_id` varchar(300) CHARACTER SET ascii DEFAULT NULL, + `ap_id` varchar(300) CHARACTER SET ascii COLLATE ascii_general_ci DEFAULT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `content_warning` text, `updated_at` timestamp NULL DEFAULT NULL, `reply_key` varbinary(1024) DEFAULT NULL, `mentions` varbinary(1024) DEFAULT NULL, - `reply_count` int(10) unsigned NOT NULL DEFAULT '0', + `reply_count` int unsigned NOT NULL DEFAULT '0', `ap_replies` varchar(300) DEFAULT NULL, - `poll_id` int(10) unsigned DEFAULT NULL, - `federation_state` tinyint(10) unsigned NOT NULL DEFAULT '0', + `poll_id` int unsigned DEFAULT NULL, + `federation_state` tinyint unsigned NOT NULL DEFAULT '0', `source` text, - `source_format` tinyint(3) unsigned DEFAULT NULL, + `source_format` tinyint unsigned DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ap_id` (`ap_id`), KEY `owner_user_id` (`owner_user_id`), @@ -513,24 +473,10 @@ CREATE TABLE `wall_posts` ( KEY `reply_key` (`reply_key`), KEY `owner_group_id` (`owner_group_id`), CONSTRAINT `wall_posts_ibfk_1` FOREIGN KEY (`owner_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, - CONSTRAINT `wall_posts_ibfk_2` FOREIGN KEY (`repost_of`) REFERENCES `wall_posts` (`id`) ON DELETE SET NULL ON UPDATE NO ACTION, + CONSTRAINT `wall_posts_ibfk_2` FOREIGN KEY (`repost_of`) REFERENCES `wall_posts` (`id`) ON DELETE SET NULL, CONSTRAINT `wall_posts_ibfk_3` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, CONSTRAINT `wall_posts_ibfk_4` FOREIGN KEY (`owner_group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - - --- --- Dumping routines (FUNCTION) for database 'smithereen' --- -DELIMITER ;; - -DELIMITER ; - -/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; -/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; -/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; -/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +-- Dump completed on 2021-12-15 21:51:09 diff --git a/src/main/java/smithereen/Mailer.java b/src/main/java/smithereen/Mailer.java index f7a2b688..f01befbf 100644 --- a/src/main/java/smithereen/Mailer.java +++ b/src/main/java/smithereen/Mailer.java @@ -78,7 +78,7 @@ public void sendPasswordReset(Request req, Account account, String code){ Lang l=Utils.lang(req); String link=UriBuilder.local().appendPath("account").appendPath("actuallyResetPassword").queryParam("code", code).build().toString(); String plaintext=l.get("email_password_reset_plain_before", Map.of("name", account.user.firstName, "serverName", Config.domain))+"\n\n"+link+"\n\n"+l.get("email_password_reset_after"); - send(account.email, l.get("email_password_reset_subject", Map.of("serverName", Config.domain)), plaintext, "reset_password", Map.of( + send(account.email, l.get("email_password_reset_subject", Map.of("domain", Config.domain)), plaintext, "reset_password", Map.of( "domain", Config.domain, "serverName", Config.serverDisplayName, "name", account.user.firstName, diff --git a/src/main/java/smithereen/SmithereenApplication.java b/src/main/java/smithereen/SmithereenApplication.java index 9b3b4d91..30524e27 100644 --- a/src/main/java/smithereen/SmithereenApplication.java +++ b/src/main/java/smithereen/SmithereenApplication.java @@ -12,7 +12,6 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.sql.SQLException; -import java.sql.Timestamp; import java.time.Instant; import java.util.Arrays; import java.util.Collections; @@ -31,6 +30,7 @@ import smithereen.exceptions.FloodControlViolationException; import smithereen.exceptions.ObjectNotFoundException; import smithereen.exceptions.UserActionNotAllowedException; +import smithereen.exceptions.UserErrorException; import smithereen.routes.ActivityPubRoutes; import smithereen.routes.ApiRoutes; import smithereen.routes.GroupsRoutes; @@ -323,7 +323,8 @@ public static void main(String[] args){ get("/inbox", SmithereenApplication::methodNotAllowed); getActivityPubCollection("/outbox", 50, ActivityPubRoutes::groupOutbox); post("/outbox", SmithereenApplication::methodNotAllowed); - getActivityPubCollection("/followers", 50, ActivityPubRoutes::groupFollowers); + getActivityPubCollection("/members", 50, ActivityPubRoutes::groupMembers); + getActivityPubCollection("/tentativeMembers", 50, ActivityPubRoutes::groupTentativeMembers); getActivityPubCollection("/wall", 50, ActivityPubRoutes::groupWall); getLoggedIn("/edit", GroupsRoutes::editGeneral); @@ -347,6 +348,7 @@ public static void main(String[] args){ postWithCSRF("/unblockDomain", GroupsRoutes::unblockDomain); get("/members", GroupsRoutes::members); + get("/tentativeMembers", GroupsRoutes::tentativeMembers); get("/admins", GroupsRoutes::admins); path("/wall", ()->{ get("", PostRoutes::groupWall); @@ -448,6 +450,9 @@ public static void main(String[] args){ resp.status(429); resp.body(Utils.wrapErrorString(req, resp, Objects.requireNonNullElse(x.getMessage(), "err_flood_control"))); }); + exception(UserErrorException.class, (x, req, resp)->{ + resp.body(Utils.wrapErrorString(req, resp, x.getMessage())); + }); exception(Exception.class, (exception, req, res) -> { LOG.warn("Exception while processing {} {}", req.requestMethod(), req.raw().getPathInfo(), exception); res.status(500); diff --git a/src/main/java/smithereen/Utils.java b/src/main/java/smithereen/Utils.java index 96cdd448..44801ad0 100644 --- a/src/main/java/smithereen/Utils.java +++ b/src/main/java/smithereen/Utils.java @@ -38,6 +38,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -780,6 +781,28 @@ public static Instant instantFromDateAndTime(Request req, String dateStr, String return LocalDateTime.of(date, time).atZone(timeZoneForRequest(req).toZoneId()).toInstant(); } + public static > long serializeEnumSet(EnumSet set, Class cls){ + if(cls.getEnumConstants().length>64) + throw new IllegalArgumentException("this enum has more than 64 constants"); + long result=0; + for(E value:set){ + result|=1L << value.ordinal(); + } + return result; + } + + public static > void deserializeEnumSet(EnumSet set, Class cls, long l){ + set.clear(); + E[] consts=cls.getEnumConstants(); + if(consts.length>64) + throw new IllegalArgumentException("this enum has more than 64 constants"); + for(E e:consts){ + if((l&1)==1) + set.add(e); + l >>= 1; + } + } + public interface MentionCallback{ User resolveMention(String username, String domain); User resolveMention(String uri); diff --git a/src/main/java/smithereen/activitypub/ActivityPubWorker.java b/src/main/java/smithereen/activitypub/ActivityPubWorker.java index f273afef..167bb5bc 100644 --- a/src/main/java/smithereen/activitypub/ActivityPubWorker.java +++ b/src/main/java/smithereen/activitypub/ActivityPubWorker.java @@ -9,29 +9,22 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.Set; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.ForkJoinPool; -import java.util.concurrent.ForkJoinTask; import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; import java.util.concurrent.RecursiveTask; import java.util.function.Consumer; -import java.util.stream.Collectors; import smithereen.Config; import smithereen.ObjectLinkResolver; import smithereen.Utils; import smithereen.activitypub.objects.Activity; import smithereen.activitypub.objects.ActivityPubCollection; -import smithereen.activitypub.objects.ActivityPubObject; import smithereen.activitypub.objects.Actor; import smithereen.activitypub.objects.CollectionPage; import smithereen.activitypub.objects.LinkOrObject; @@ -41,6 +34,8 @@ import smithereen.activitypub.objects.activities.Create; import smithereen.activitypub.objects.activities.Delete; import smithereen.activitypub.objects.activities.Follow; +import smithereen.activitypub.objects.activities.Join; +import smithereen.activitypub.objects.activities.Leave; import smithereen.activitypub.objects.activities.Like; import smithereen.activitypub.objects.activities.Offer; import smithereen.activitypub.objects.activities.Reject; @@ -348,7 +343,7 @@ public void sendRemoveFromGroupsCollectionActivity(User self, Group group){ } } - public void sendFollowActivity(User self, ForeignUser target){ + public void sendFollowUserActivity(User self, ForeignUser target){ Follow follow=new Follow(); follow.actor=new LinkOrObject(self.activityPubID); follow.object=new LinkOrObject(target.activityPubID); @@ -356,25 +351,34 @@ public void sendFollowActivity(User self, ForeignUser target){ executor.submit(new SendOneActivityRunnable(follow, target.inbox, self)); } - public void sendFollowActivity(User self, ForeignGroup target){ - Follow follow=new Follow(); + public void sendJoinGroupActivity(User self, ForeignGroup target, boolean tentative){ + Activity follow; + if(target.hasCapability(ForeignGroup.Capability.JOIN_LEAVE_ACTIVITIES)) + follow=new Join(tentative); + else + follow=new Follow(); follow.actor=new LinkOrObject(self.activityPubID); follow.object=new LinkOrObject(target.activityPubID); follow.activityPubID=new UriBuilder(self.activityPubID).fragment("joinGroup"+target.id+"_"+rand()).build(); executor.submit(new SendOneActivityRunnable(follow, target.inbox, self)); } - public void sendUnfollowActivity(User self, ForeignGroup target){ - Undo undo=new Undo(); + public void sendLeaveGroupActivity(User self, ForeignGroup target){ + Activity undo; + if(target.hasCapability(ForeignGroup.Capability.JOIN_LEAVE_ACTIVITIES)){ + undo=new Leave(); + undo.object=new LinkOrObject(target.activityPubID); + }else{ + undo=new Undo(); + Follow follow=new Follow(); + follow.actor=new LinkOrObject(self.activityPubID); + follow.object=new LinkOrObject(target.activityPubID); + follow.activityPubID=new UriBuilder(self.activityPubID).fragment("joinGroup"+target.id+"_"+rand()).build(); + undo.object=new LinkOrObject(follow); + } undo.activityPubID=new UriBuilder(self.activityPubID).fragment("leaveGroup"+target.id+"_"+rand()).build(); undo.actor=new LinkOrObject(self.activityPubID); - Follow follow=new Follow(); - follow.actor=new LinkOrObject(self.activityPubID); - follow.object=new LinkOrObject(target.activityPubID); - follow.activityPubID=new UriBuilder(self.activityPubID).fragment("joinGroup"+target.id+"_"+rand()).build(); - undo.object=new LinkOrObject(follow); - executor.submit(new SendOneActivityRunnable(undo, target.inbox, self)); } diff --git a/src/main/java/smithereen/activitypub/handlers/FollowGroupHandler.java b/src/main/java/smithereen/activitypub/handlers/FollowGroupHandler.java index fd57e24a..a8f2ee4f 100644 --- a/src/main/java/smithereen/activitypub/handlers/FollowGroupHandler.java +++ b/src/main/java/smithereen/activitypub/handlers/FollowGroupHandler.java @@ -3,6 +3,7 @@ import java.sql.SQLException; import smithereen.Utils; +import smithereen.activitypub.objects.activities.Join; import smithereen.exceptions.BadRequestException; import smithereen.activitypub.ActivityHandlerContext; import smithereen.activitypub.ActivityPubWorker; @@ -20,14 +21,19 @@ public void handle(ActivityHandlerContext context, ForeignUser actor, Follow act throw new BadRequestException("Follow is only supported for local groups"); Utils.ensureUserNotBlocked(actor, group); + boolean tentative=group.isEvent() && activity instanceof Join j && j.tentative; + Group.MembershipState state=GroupStorage.getUserMembershipState(group.id, actor.id); if(state==Group.MembershipState.MEMBER || state==Group.MembershipState.TENTATIVE_MEMBER){ // send an Accept{Follow} once again because the other server apparently didn't get it the first time // why would it resend a Follow otherwise? ActivityPubWorker.getInstance().sendAcceptFollowActivity(actor, group, activity); + // update the event decision locally if it changed + if((tentative && state==Group.MembershipState.MEMBER) || (!tentative && state==Group.MembershipState.TENTATIVE_MEMBER)) + GroupStorage.updateUserEventDecision(group, actor.id, tentative); return; } - GroupStorage.joinGroup(group, actor.id, false, true); + GroupStorage.joinGroup(group, actor.id, tentative, true); ActivityPubWorker.getInstance().sendAcceptFollowActivity(actor, group, activity); } } diff --git a/src/main/java/smithereen/activitypub/objects/ActivityPubObject.java b/src/main/java/smithereen/activitypub/objects/ActivityPubObject.java index 675f7990..ee6e1de6 100644 --- a/src/main/java/smithereen/activitypub/objects/ActivityPubObject.java +++ b/src/main/java/smithereen/activitypub/objects/ActivityPubObject.java @@ -566,7 +566,8 @@ public static ActivityPubObject parse(JsonObject obj, ParserContext parserContex case "Create" -> new Create(); case "Delete" -> new Delete(); case "Follow" -> new Follow(); - case "Join" -> new Join(); + case "Join" -> new Join(false); + case "TentativeJoin" -> new Join(true); case "Like" -> new Like(); case "Undo" -> new Undo(); case "Update" -> new Update(); diff --git a/src/main/java/smithereen/activitypub/objects/Actor.java b/src/main/java/smithereen/activitypub/objects/Actor.java index 20b30fa7..475cb2e8 100644 --- a/src/main/java/smithereen/activitypub/objects/Actor.java +++ b/src/main/java/smithereen/activitypub/objects/Actor.java @@ -123,7 +123,7 @@ public JsonObject asActivityPubObject(JsonObject obj, ContextCollector contextCo obj.addProperty("inbox", userURL+"/inbox"); obj.addProperty("outbox", userURL+"/outbox"); if(canBeFollowed()) - obj.addProperty("followers", userURL+"/followers"); + obj.addProperty("followers", getFollowersURL().toString()); if(canFollowOtherActors()) obj.addProperty("following", userURL+"/following"); diff --git a/src/main/java/smithereen/activitypub/objects/activities/Join.java b/src/main/java/smithereen/activitypub/objects/activities/Join.java index 72f22ab8..e4cb2b6b 100644 --- a/src/main/java/smithereen/activitypub/objects/activities/Join.java +++ b/src/main/java/smithereen/activitypub/objects/activities/Join.java @@ -1,8 +1,26 @@ package smithereen.activitypub.objects.activities; +import com.google.gson.JsonObject; + +import smithereen.activitypub.ContextCollector; +import smithereen.jsonld.JLD; + public class Join extends Follow{ + public boolean tentative; + + public Join(boolean tentative){ + this.tentative=tentative; + } + @Override public String getType(){ - return "Join"; + return tentative ? "TentativeJoin" : "Join"; + } + + @Override + public JsonObject asActivityPubObject(JsonObject obj, ContextCollector contextCollector){ + contextCollector.addAlias("TentativeJoin", "sm:TentativeJoin"); + contextCollector.addAlias("sm", JLD.SMITHEREEN); + return super.asActivityPubObject(obj, contextCollector); } } diff --git a/src/main/java/smithereen/controllers/GroupsController.java b/src/main/java/smithereen/controllers/GroupsController.java index dd94de50..ca4e3ca6 100644 --- a/src/main/java/smithereen/controllers/GroupsController.java +++ b/src/main/java/smithereen/controllers/GroupsController.java @@ -19,13 +19,17 @@ import smithereen.data.PaginatedList; import smithereen.data.User; import smithereen.data.feed.NewsfeedEntry; +import smithereen.exceptions.BadRequestException; import smithereen.exceptions.InternalServerErrorException; import smithereen.exceptions.ObjectNotFoundException; import smithereen.exceptions.UserActionNotAllowedException; +import smithereen.exceptions.UserErrorException; import smithereen.storage.GroupStorage; import smithereen.storage.NewsfeedStorage; import spark.utils.StringUtils; +import static smithereen.Utils.wrapError; + public class GroupsController{ private static final Logger LOG=LoggerFactory.getLogger(GroupsController.class); @@ -104,9 +108,17 @@ public Group getLocalGroupOrThrow(int id){ return group; } - public PaginatedList getMembers(@NotNull Group group, int offset, int count){ + public PaginatedList getMembers(@NotNull Group group, int offset, int count, boolean tentative){ + try{ + return GroupStorage.getMembers(group.id, offset, count, tentative); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public PaginatedList getAllMembers(@NotNull Group group, int offset, int count){ try{ - return GroupStorage.getMembers(group.id, offset, count); + return GroupStorage.getMembers(group.id, offset, count, null); }catch(SQLException x){ throw new InternalServerErrorException(x); } @@ -120,9 +132,9 @@ public List getAdmins(@NotNull Group group){ } } - public List getRandomMembersForProfile(@NotNull Group group){ + public List getRandomMembersForProfile(@NotNull Group group, boolean tentative){ try{ - return GroupStorage.getRandomMembersForProfile(group.id); + return GroupStorage.getRandomMembersForProfile(group.id, tentative); }catch(SQLException x){ throw new InternalServerErrorException(x); } @@ -162,6 +174,60 @@ public void updateGroupInfo(@NotNull Group group, @NotNull User admin, String na } } + public void joinGroup(@NotNull Group group, @NotNull User user, boolean tentative){ + try{ + Utils.ensureUserNotBlocked(user, group); + + Group.MembershipState state=GroupStorage.getUserMembershipState(group.id, user.id); + if(group.isEvent()){ + if((state==Group.MembershipState.MEMBER && !tentative) || (state==Group.MembershipState.TENTATIVE_MEMBER && tentative)) + throw new UserErrorException("err_group_already_member"); + }else{ + if(state==Group.MembershipState.MEMBER || state==Group.MembershipState.TENTATIVE_MEMBER) + throw new UserErrorException("err_group_already_member"); + } + + if(tentative && (!group.isEvent() || (group instanceof ForeignGroup fg && !fg.hasCapability(ForeignGroup.Capability.TENTATIVE_MEMBERSHIP)))) + throw new BadRequestException(); + + // change certain <-> tentative + if(state==Group.MembershipState.MEMBER || state==Group.MembershipState.TENTATIVE_MEMBER){ + GroupStorage.updateUserEventDecision(group, user.id, tentative); + if(group instanceof ForeignGroup fg) + ActivityPubWorker.getInstance().sendJoinGroupActivity(user, fg, tentative); + return; + } + + GroupStorage.joinGroup(group, user.id, tentative, !(group instanceof ForeignGroup)); + if(group instanceof ForeignGroup fg){ + // Add{Group} will be sent upon receiving Accept{Follow} + ActivityPubWorker.getInstance().sendJoinGroupActivity(user, fg, tentative); + }else{ + ActivityPubWorker.getInstance().sendAddToGroupsCollectionActivity(user, group); + } + NewsfeedStorage.putEntry(user.id, group.id, group.isEvent() ? NewsfeedEntry.Type.JOIN_EVENT : NewsfeedEntry.Type.JOIN_GROUP, null); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public void leaveGroup(@NotNull Group group, @NotNull User user){ + try{ + Group.MembershipState state=GroupStorage.getUserMembershipState(group.id, user.id); + if(state!=Group.MembershipState.MEMBER && state!=Group.MembershipState.TENTATIVE_MEMBER){ + throw new UserErrorException("err_group_not_member"); + } + GroupStorage.leaveGroup(group, user.id, state==Group.MembershipState.TENTATIVE_MEMBER); + if(group instanceof ForeignGroup fg){ + ActivityPubWorker.getInstance().sendLeaveGroupActivity(user, fg); + } + ActivityPubWorker.getInstance().sendRemoveFromGroupsCollectionActivity(user, group); + NewsfeedStorage.deleteEntry(user.id, group.id, group.isEvent() ? NewsfeedEntry.Type.JOIN_EVENT : NewsfeedEntry.Type.JOIN_GROUP); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + public enum EventsType{ FUTURE, PAST, diff --git a/src/main/java/smithereen/data/ForeignGroup.java b/src/main/java/smithereen/data/ForeignGroup.java index 1454ab51..cf1aaf7b 100644 --- a/src/main/java/smithereen/data/ForeignGroup.java +++ b/src/main/java/smithereen/data/ForeignGroup.java @@ -8,6 +8,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.EnumSet; import java.util.Objects; import smithereen.ObjectLinkResolver; @@ -22,6 +23,7 @@ public class ForeignGroup extends Group implements ForeignActor{ private URI wall; + public EnumSet capabilities=EnumSet.noneOf(ForeignGroup.Capability.class); public static ForeignGroup fromResultSet(ResultSet res) throws SQLException{ ForeignGroup g=new ForeignGroup(); @@ -41,6 +43,7 @@ protected void fillFromResultSet(ResultSet res) throws SQLException{ followers=tryParseURL(res.getString("ap_followers")); lastUpdated=res.getTimestamp("last_updated"); wall=tryParseURL(res.getString("ap_wall")); + Utils.deserializeEnumSet(capabilities, ForeignGroup.Capability.class, res.getLong("flags")); } @Override @@ -61,6 +64,7 @@ protected ActivityPubObject parseActivityPubObject(JsonObject obj, ParserContext } } wall=tryParseURL(optString(obj, "wall")); + ensureHostMatchesID(wall, "wall"); if(attachment!=null && !attachment.isEmpty()){ for(ActivityPubObject att:attachment){ @@ -72,6 +76,14 @@ protected ActivityPubObject parseActivityPubObject(JsonObject obj, ParserContext } } + if(obj.has("capabilities")){ + JsonObject caps=obj.getAsJsonObject("capabilities"); + if(optBoolean(caps, "acceptsJoins")) + capabilities.add(Capability.JOIN_LEAVE_ACTIVITIES); + if(optBoolean(caps, "tentativeMembership")) + capabilities.add(Capability.TENTATIVE_MEMBERSHIP); + } + return this; } @@ -135,4 +147,24 @@ public void storeDependencies() throws SQLException{ public boolean needUpdate(){ return lastUpdated!=null && System.currentTimeMillis()-lastUpdated.getTime()>24L*60*60*1000; } + + public boolean hasCapability(Capability cap){ + return capabilities.contains(cap); + } + + // for use from templates + public boolean hasCapability(String cap){ + return hasCapability(Capability.valueOf(cap)); + } + + public enum Capability{ + /** + * Supports Join{Group} and Leave{Group} instead of Follow{Group}/Undo{Follow{Group}} + */ + JOIN_LEAVE_ACTIVITIES, + /** + * Supports tentative memberships (sm:TentativeJoin for joining and TentativeAccept for accepting invites) + */ + TENTATIVE_MEMBERSHIP + } } diff --git a/src/main/java/smithereen/data/Group.java b/src/main/java/smithereen/data/Group.java index 9c43bdef..a6bff78f 100644 --- a/src/main/java/smithereen/data/Group.java +++ b/src/main/java/smithereen/data/Group.java @@ -7,9 +7,11 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.time.Instant; +import java.util.EnumSet; import java.util.List; import smithereen.Config; +import smithereen.Utils; import smithereen.activitypub.ContextCollector; import smithereen.activitypub.objects.Actor; import smithereen.activitypub.objects.Event; @@ -20,7 +22,7 @@ public class Group extends Actor{ public int id; - public int memberCount; + public int memberCount, tentativeMemberCount; public Type type=Type.GROUP; public Instant eventStartTime, eventEndTime; @@ -69,6 +71,7 @@ protected void fillFromResultSet(ResultSet res) throws SQLException{ activityPubID=Config.localURI("/groups/"+id); url=Config.localURI(username); memberCount=res.getInt("member_count"); + tentativeMemberCount=res.getInt("tentative_member_count"); summary=res.getString("about"); eventStartTime=DatabaseUtils.getInstant(res, "event_start_time"); eventEndTime=DatabaseUtils.getInstant(res, "event_end_time"); @@ -98,10 +101,17 @@ public JsonObject asActivityPubObject(JsonObject obj, ContextCollector contextCo } obj.add("attributedTo", ar); - obj.addProperty("members", userURL+"/followers"); + obj.addProperty("members", userURL+"/members"); contextCollector.addType("members", "sm:members", "@id"); - JsonObject capabilities=new JsonObject(); + + if(type==Type.EVENT){ + obj.addProperty("tentativeMembers", userURL+"/tentativeMembers"); + contextCollector.addType("tentativeMembers", "sm:tentativeMembers", "@id"); + capabilities.addProperty("tentativeMembership", true); + contextCollector.addAlias("tentativeMembership", "sm:tentativeMembership"); + } + capabilities.addProperty("acceptsJoins", true); obj.add("capabilities", capabilities); contextCollector.addAlias("capabilities", "litepub:capabilities"); @@ -115,6 +125,12 @@ public boolean isEvent(){ return type==Type.EVENT; } + @Override + public URI getFollowersURL(){ + String userURL=activityPubID.toString(); + return URI.create(userURL+"/members"); + } + public enum AdminLevel{ REGULAR, MODERATOR, @@ -141,4 +157,5 @@ public enum Type{ GROUP, EVENT } + } diff --git a/src/main/java/smithereen/exceptions/UserErrorException.java b/src/main/java/smithereen/exceptions/UserErrorException.java new file mode 100644 index 00000000..5d49d69b --- /dev/null +++ b/src/main/java/smithereen/exceptions/UserErrorException.java @@ -0,0 +1,18 @@ +package smithereen.exceptions; + +public class UserErrorException extends RuntimeException{ + public UserErrorException(){ + } + + public UserErrorException(String message){ + super(message); + } + + public UserErrorException(String message, Throwable cause){ + super(message, cause); + } + + public UserErrorException(Throwable cause){ + super(cause); + } +} diff --git a/src/main/java/smithereen/jsonld/JLDProcessor.java b/src/main/java/smithereen/jsonld/JLDProcessor.java index 3561febd..5901d723 100644 --- a/src/main/java/smithereen/jsonld/JLDProcessor.java +++ b/src/main/java/smithereen/jsonld/JLDProcessor.java @@ -68,6 +68,13 @@ public class JLDProcessor{ lc.add("friends", idAndTypeObject("sm:friends", "@id")); lc.add("groups", idAndTypeObject("sm:groups", "@id")); lc.addProperty("nonAnonymous", "sm:nonAnonymous"); + lc.addProperty("tentativeMembership", "sm:tentativeMembership"); + lc.add("tentativeMembers", idAndTypeObject("sm:tentativeMembers", "@id")); + lc.addProperty("TentativeJoin", "sm:TentativeJoin"); + + // litepub aliases + lc.addProperty("capabilities", "litepub:capabilities"); + lc.addProperty("acceptsJoins", "litepub:acceptsJoins"); localContext=updateContext(new JLDContext(), makeArray(JLD.ACTIVITY_STREAMS, JLD.W3_SECURITY, lc), new ArrayList<>(), null); inverseLocalContext=createReverseContext(localContext); diff --git a/src/main/java/smithereen/routes/ActivityPubRoutes.java b/src/main/java/smithereen/routes/ActivityPubRoutes.java index f70112b7..df39689d 100644 --- a/src/main/java/smithereen/routes/ActivityPubRoutes.java +++ b/src/main/java/smithereen/routes/ActivityPubRoutes.java @@ -126,9 +126,11 @@ import spark.Response; import spark.utils.StringUtils; +import static smithereen.Utils.context; import static smithereen.Utils.gson; import static smithereen.Utils.parseIntOrDefault; import static smithereen.Utils.parseSignatureHeader; +import static smithereen.Utils.safeParseInt; public class ActivityPubRoutes{ @@ -791,15 +793,19 @@ public static ActivityPubCollectionPageResponse userFollowing(Request req, Respo return followersOrFollowing(req, resp, false, offset, count); } - public static ActivityPubCollectionPageResponse groupFollowers(Request req, Response resp, int offset, int count) throws SQLException{ - int id=Utils.parseIntOrDefault(req.params(":id"), 0); - Group group=GroupStorage.getById(id); - if(group==null || group instanceof ForeignGroup){ - throw new ObjectNotFoundException(); - } - int[] _total={0}; - List followers=GroupStorage.getGroupMemberURIs(group.id, false, offset, count, _total); - return ActivityPubCollectionPageResponse.forLinks(followers, _total[0]); + private static ActivityPubCollectionPageResponse groupMembers(Request req, Response resp, int offset, int count, boolean tentative) throws SQLException{ + int id=safeParseInt(req.params(":id")); + Group group=context(req).getGroupsController().getLocalGroupOrThrow(id); + PaginatedList followers=GroupStorage.getGroupMemberURIs(group.id, tentative, offset, count); + return ActivityPubCollectionPageResponse.forLinks(followers); + } + + public static ActivityPubCollectionPageResponse groupMembers(Request req, Response resp, int offset, int count) throws SQLException{ + return groupMembers(req, resp, offset, count, false); + } + + public static ActivityPubCollectionPageResponse groupTentativeMembers(Request req, Response resp, int offset, int count) throws SQLException{ + return groupMembers(req, resp, offset, count, true); } public static Object serviceActor(Request req, Response resp) throws SQLException{ diff --git a/src/main/java/smithereen/routes/GroupsRoutes.java b/src/main/java/smithereen/routes/GroupsRoutes.java index 89cdb0f9..93aae44f 100644 --- a/src/main/java/smithereen/routes/GroupsRoutes.java +++ b/src/main/java/smithereen/routes/GroupsRoutes.java @@ -159,7 +159,7 @@ public static Object groupProfile(Request req, Response resp, Group group){ SessionInfo info=Utils.sessionInfo(req); @Nullable Account self=info!=null ? info.account : null; - List members=context(req).getGroupsController().getRandomMembersForProfile(group); + List members=context(req).getGroupsController().getRandomMembersForProfile(group, false); int offset=offset(req); PaginatedList wall=context(req).getWallController().getWallPosts(group, false, offset, 20); @@ -172,6 +172,8 @@ public static Object groupProfile(Request req, Response resp, Group group){ RenderedTemplateResponse model=new RenderedTemplateResponse("group", req); model.with("group", group).with("members", members).with("postCount", wall.total).paginate(wall); + if(group.isEvent()) + model.with("tentativeMembers", context(req).getGroupsController().getRandomMembersForProfile(group, true)); model.with("postInteractions", interactions); model.with("title", group.name); model.with("admins", context(req).getGroupsController().getAdmins(group)); @@ -181,13 +183,20 @@ public static Object groupProfile(Request req, Response resp, Group group){ jsLangKey(req, "create_poll_question", "create_poll_options", "create_poll_add_option", "create_poll_delete_option", "create_poll_multi_choice", "create_poll_anonymous", "create_poll_time_limit", "X_days", "X_hours"); if(self!=null){ Group.AdminLevel level=context(req).getGroupsController().getMemberAdminLevel(group, self.user); - model.with("membershipState", context(req).getGroupsController().getUserMembershipState(group, self.user)); + Group.MembershipState membershipState=context(req).getGroupsController().getUserMembershipState(group, self.user); + model.with("membershipState", membershipState); model.with("groupAdminLevel", level); if(level.isAtLeast(Group.AdminLevel.ADMIN)){ jsLangKey(req, "update_profile_picture", "save", "profile_pic_select_square_version", "drag_or_choose_file", "choose_file", "drop_files_here", "picture_too_wide", "picture_too_narrow", "ok", "error", "error_loading_picture", "remove_profile_picture", "confirm_remove_profile_picture", "choose_file_mobile"); } + if(group.isEvent()){ + if(membershipState==Group.MembershipState.MEMBER) + model.with("membershipStateText", l.get("event_joined_certain")); + else if(membershipState==Group.MembershipState.TENTATIVE_MEMBER) + model.with("membershipStateText", l.get("event_joined_tentative")); + } }else{ HashMap meta=new LinkedHashMap<>(); meta.put("og:type", "profile"); @@ -228,20 +237,9 @@ public static Object groupProfile(Request req, Response resp, Group group){ return model; } - public static Object join(Request req, Response resp, Account self) throws SQLException{ + public static Object join(Request req, Response resp, Account self){ Group group=getGroup(req); - ensureUserNotBlocked(self.user, group); - Group.MembershipState state=GroupStorage.getUserMembershipState(group.id, self.user.id); - if(state==Group.MembershipState.MEMBER || state==Group.MembershipState.TENTATIVE_MEMBER){ - return wrapError(req, resp, "err_group_already_member"); - } - GroupStorage.joinGroup(group, self.user.id, false, !(group instanceof ForeignGroup)); - if(group instanceof ForeignGroup){ - ActivityPubWorker.getInstance().sendFollowActivity(self.user, (ForeignGroup) group); - }else{ - ActivityPubWorker.getInstance().sendAddToGroupsCollectionActivity(self.user, group); - } - NewsfeedStorage.putEntry(self.user.id, group.id, group.isEvent() ? NewsfeedEntry.Type.JOIN_EVENT : NewsfeedEntry.Type.JOIN_GROUP, null); + context(req).getGroupsController().joinGroup(group, self.user, "1".equals(req.queryParams("tentative"))); if(isAjax(req)){ return new WebDeltaResponse(resp).refresh(); } @@ -251,17 +249,7 @@ public static Object join(Request req, Response resp, Account self) throws SQLEx public static Object leave(Request req, Response resp, Account self) throws SQLException{ Group group=getGroup(req); - Group.MembershipState state=GroupStorage.getUserMembershipState(group.id, self.user.id); - if(state!=Group.MembershipState.MEMBER && state!=Group.MembershipState.TENTATIVE_MEMBER){ - return wrapError(req, resp, "err_group_not_member"); - } - GroupStorage.leaveGroup(group, self.user.id, state==Group.MembershipState.TENTATIVE_MEMBER); - if(group instanceof ForeignGroup){ - ActivityPubWorker.getInstance().sendUnfollowActivity(self.user, (ForeignGroup) group); - }else{ - ActivityPubWorker.getInstance().sendRemoveFromGroupsCollectionActivity(self.user, group); - } - NewsfeedStorage.deleteEntry(self.user.id, group.id, group.isEvent() ? NewsfeedEntry.Type.JOIN_EVENT : NewsfeedEntry.Type.JOIN_GROUP); + context(req).getGroupsController().leaveGroup(group, self.user); if(isAjax(req)){ return new WebDeltaResponse(resp).refresh(); } @@ -324,16 +312,20 @@ public static Object saveGeneral(Request req, Response resp, Account self){ } public static Object members(Request req, Response resp){ + return members(req, resp, false); + } + + public static Object tentativeMembers(Request req, Response resp){ + return members(req, resp, true); + } + + private static Object members(Request req, Response resp, boolean tentative){ Group group=getGroup(req); + if(tentative && !group.isEvent()) + throw new BadRequestException(); RenderedTemplateResponse model=new RenderedTemplateResponse(isAjax(req) ? "user_grid" : "content_wrap", req); - model.paginate(context(req).getGroupsController().getMembers(group, offset(req), 100)); - model.with("summary", lang(req).get("summary_group_X_members", Map.of("count", group.memberCount))); -// if(isAjax(req)){ -// if(req.queryParams("fromPagination")==null) -// return new WebDeltaResponseBuilder(resp).box(lang(req).get("likes_title"), model, "likesList", 596); -// else -// return new WebDeltaResponseBuilder(resp).setContent("likesList", model); -// } + model.paginate(context(req).getGroupsController().getMembers(group, offset(req), 100, tentative)); + model.with("summary", lang(req).get(tentative ? "summary_event_X_tentative_members" : (group.isEvent() ? "summary_event_X_members" : "summary_group_X_members"), Map.of("count", tentative ? group.tentativeMemberCount : group.memberCount))); model.with("contentTemplate", "user_grid").with("title", group.name); return model; } @@ -361,7 +353,7 @@ public static Object editMembers(Request req, Response resp, Account self){ Group group=getGroupAndRequireLevel(req, self, Group.AdminLevel.MODERATOR); Group.AdminLevel level=context(req).getGroupsController().getMemberAdminLevel(group, self.user); RenderedTemplateResponse model=new RenderedTemplateResponse("group_edit_members", req); - model.paginate(context(req).getGroupsController().getMembers(group, offset(req), 100)); + model.paginate(context(req).getGroupsController().getAllMembers(group, offset(req), 100)); model.with("group", group).with("title", group.name); model.with("adminIDs", context(req).getGroupsController().getAdmins(group).stream().map(adm->adm.user.id).collect(Collectors.toList())); model.with("canAddAdmins", level.isAtLeast(Group.AdminLevel.ADMIN)); diff --git a/src/main/java/smithereen/routes/ProfileRoutes.java b/src/main/java/smithereen/routes/ProfileRoutes.java index 0aba0861..c98d2e63 100644 --- a/src/main/java/smithereen/routes/ProfileRoutes.java +++ b/src/main/java/smithereen/routes/ProfileRoutes.java @@ -12,8 +12,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; import static smithereen.Utils.*; @@ -23,7 +21,6 @@ import smithereen.activitypub.objects.PropertyValue; import smithereen.data.Account; import smithereen.data.ForeignUser; -import smithereen.data.FriendRequest; import smithereen.data.FriendshipStatus; import smithereen.data.Group; import smithereen.data.PaginatedList; @@ -40,7 +37,6 @@ import smithereen.storage.GroupStorage; import smithereen.storage.NewsfeedStorage; import smithereen.storage.NotificationsStorage; -import smithereen.storage.PostStorage; import smithereen.storage.UserStorage; import smithereen.templates.RenderedTemplateResponse; import spark.Request; @@ -194,7 +190,7 @@ public static Object confirmSendFriendRequest(Request req, Response resp, Accoun if(isAjax(req) && verifyCSRF(req, resp)){ UserStorage.followUser(self.user.id, user.id, !(user instanceof ForeignUser)); if(user instanceof ForeignUser){ - ActivityPubWorker.getInstance().sendFollowActivity(self.user, (ForeignUser) user); + ActivityPubWorker.getInstance().sendFollowUserActivity(self.user, (ForeignUser) user); } return new WebDeltaResponse(resp).refresh(); }else{ @@ -241,7 +237,7 @@ public static Object doSendFriendRequest(Request req, Response resp, Account sel }else{ UserStorage.followUser(self.user.id, user.id, !(user instanceof ForeignUser)); if(user instanceof ForeignUser){ - ActivityPubWorker.getInstance().sendFollowActivity(self.user, (ForeignUser)user); + ActivityPubWorker.getInstance().sendFollowUserActivity(self.user, (ForeignUser)user); }else{ ActivityPubWorker.getInstance().sendAddToFriendsCollectionActivity(self.user, user); } @@ -386,7 +382,7 @@ public static Object respondToFriendRequest(Request req, Response resp, Account if(req.queryParams("accept")!=null){ if(user instanceof ForeignUser){ UserStorage.acceptFriendRequest(self.user.id, user.id, false); - ActivityPubWorker.getInstance().sendFollowActivity(self.user, (ForeignUser) user); + ActivityPubWorker.getInstance().sendFollowUserActivity(self.user, (ForeignUser) user); }else{ UserStorage.acceptFriendRequest(self.user.id, user.id, true); Notification n=new Notification(); diff --git a/src/main/java/smithereen/storage/DatabaseSchemaUpdater.java b/src/main/java/smithereen/storage/DatabaseSchemaUpdater.java index 4f51a7dd..414a92d9 100644 --- a/src/main/java/smithereen/storage/DatabaseSchemaUpdater.java +++ b/src/main/java/smithereen/storage/DatabaseSchemaUpdater.java @@ -12,7 +12,7 @@ import smithereen.Utils; public class DatabaseSchemaUpdater{ - public static final int SCHEMA_VERSION=17; + public static final int SCHEMA_VERSION=18; private static final Logger LOG=LoggerFactory.getLogger(DatabaseSchemaUpdater.class); public static void maybeUpdate() throws SQLException{ @@ -285,6 +285,8 @@ PRIMARY KEY (`object_type`,`object_id`,`user_id`), conn.createStatement().execute("ALTER TABLE `users` ADD `about_source` TEXT NULL AFTER `about`"); conn.createStatement().execute("ALTER TABLE `groups` ADD `about_source` TEXT NULL AFTER `about`"); conn.createStatement().execute("ALTER TABLE `friend_requests` ADD `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST"); + }else if(target==18){ + conn.createStatement().execute("ALTER TABLE `groups` ADD `flags` BIGINT UNSIGNED NOT NULL DEFAULT 0"); } } } diff --git a/src/main/java/smithereen/storage/DatabaseUtils.java b/src/main/java/smithereen/storage/DatabaseUtils.java index b947a83a..11ad155f 100644 --- a/src/main/java/smithereen/storage/DatabaseUtils.java +++ b/src/main/java/smithereen/storage/DatabaseUtils.java @@ -152,6 +152,17 @@ public static LocalDate getLocalDate(ResultSet res, String name) throws SQLExcep return date==null ? null : date.toLocalDate(); } + public static void doWithTransaction(Connection conn, SQLRunnable r) throws SQLException{ + boolean success=false; + try{ + conn.createStatement().execute("START TRANSACTION"); + r.run(); + success=true; + }finally{ + conn.createStatement().execute(success ? "COMMIT" : "ROLLBACK"); + } + } + private static class UncheckedSQLException extends RuntimeException{ public UncheckedSQLException(SQLException cause){ super(cause); @@ -162,4 +173,9 @@ public synchronized SQLException getCause(){ return (SQLException) super.getCause(); } } + + @FunctionalInterface + public interface SQLRunnable{ + void run() throws SQLException; + } } diff --git a/src/main/java/smithereen/storage/GroupStorage.java b/src/main/java/smithereen/storage/GroupStorage.java index 66a8bf8a..b2deda08 100644 --- a/src/main/java/smithereen/storage/GroupStorage.java +++ b/src/main/java/smithereen/storage/GroupStorage.java @@ -145,6 +145,10 @@ public static synchronized void putOrUpdateForeignGroup(ForeignGroup group) thro .value("avatar", group.hasAvatar() ? group.icon.get(0).asActivityPubObject(new JsonObject(), new ContextCollector()).toString() : null) .value("ap_followers", Objects.toString(group.followers, null)) .value("ap_wall", Objects.toString(group.getWallURL(), null)) + .value("event_start_time", group.eventStartTime) + .value("event_end_time", group.eventEndTime) + .value("type", group.type) + .value("flags", Utils.serializeEnumSet(group.capabilities, ForeignGroup.Capability.class)) .valueExpr("last_updated", "CURRENT_TIMESTAMP()"); stmt=builder.createStatement(Statement.RETURN_GENERATED_KEYS); @@ -332,28 +336,28 @@ public static Map getById(Collection _ids) throws SQLEx } } - public static List getRandomMembersForProfile(int groupID) throws SQLException{ + public static List getRandomMembersForProfile(int groupID, boolean tentative) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); - PreparedStatement stmt=conn.prepareStatement("SELECT user_id FROM group_memberships WHERE group_id=? ORDER BY RAND() LIMIT 6"); - stmt.setInt(1, groupID); + PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT user_id FROM group_memberships WHERE group_id=? AND tentative=? ORDER BY RAND() LIMIT 6", groupID, tentative); try(ResultSet res=stmt.executeQuery()){ return UserStorage.getByIdAsList(DatabaseUtils.intResultSetToList(res)); } } - public static PaginatedList getMembers(int groupID, int offset, int count) throws SQLException{ + public static PaginatedList getMembers(int groupID, int offset, int count, @Nullable Boolean tentative) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); + String _tentative=tentative==null ? "" : (" AND tentative="+(tentative ? '1' : '0')); PreparedStatement stmt=new SQLQueryBuilder(conn) .selectFrom("group_memberships") .count() - .where("group_id=? AND accepted=1", groupID) + .where("group_id=? AND accepted=1"+_tentative, groupID) .createStatement(); int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery()); if(total==0) return PaginatedList.emptyList(count); stmt=new SQLQueryBuilder(conn) .selectFrom("group_memberships") - .where("group_id=? AND accepted=1", groupID) + .where("group_id=? AND accepted=1"+_tentative, groupID) .limit(count, offset) .createStatement(); try(ResultSet res=stmt.executeQuery()){ @@ -369,7 +373,7 @@ public static Group.MembershipState getUserMembershipState(int groupID, int user try(ResultSet res=stmt.executeQuery()){ if(!res.first()) return Group.MembershipState.NONE; - return Group.MembershipState.MEMBER; + return res.getBoolean("tentative") ? Group.MembershipState.TENTATIVE_MEMBER : Group.MembershipState.MEMBER; } } @@ -403,6 +407,29 @@ public static void joinGroup(Group group, int userID, boolean tentative, boolean } } + public static void updateUserEventDecision(Group group, int userID, boolean tentative) throws SQLException{ + Connection conn=DatabaseConnectionManager.getConnection(); + DatabaseUtils.doWithTransaction(conn, ()->{ + new SQLQueryBuilder(conn) + .update("group_memberships") + .where("user_id=? AND group_id=?", userID, group.id) + .value("tentative", tentative) + .createStatement() + .execute(); + + String memberCountFieldOld=tentative ? "member_count" : "tentative_member_count"; + String memberCountFieldNew=tentative ? "tentative_member_count" : "member_count"; + new SQLQueryBuilder(conn) + .update("groups") + .valueExpr(memberCountFieldOld, memberCountFieldOld+"-1") + .valueExpr(memberCountFieldNew, memberCountFieldNew+"+1") + .where("id=?", group.id) + .createStatement() + .execute(); + removeFromCache(group); + }); + } + public static void leaveGroup(Group group, int userID, boolean tentative) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); conn.createStatement().execute("START TRANSACTION"); @@ -502,16 +529,11 @@ public static PaginatedList getUserManagedGroups(int userID, int offset, } } - public static List getGroupMemberURIs(int groupID, boolean tentative, int offset, int count, int[] total) throws SQLException{ + public static PaginatedList getGroupMemberURIs(int groupID, boolean tentative, int offset, int count) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); PreparedStatement stmt; - if(total!=null){ - stmt=new SQLQueryBuilder(conn).selectFrom("group_memberships").count().where("group_id=? AND accepted=1 AND tentative=?", groupID, tentative).createStatement(); - try(ResultSet res=stmt.executeQuery()){ - res.first(); - total[0]=res.getInt(1); - } - } + stmt=new SQLQueryBuilder(conn).selectFrom("group_memberships").count().where("group_id=? AND accepted=1 AND tentative=?", groupID, tentative).createStatement(); + int total=DatabaseUtils.oneFieldToInt(stmt.executeQuery()); if(count>0){ stmt=conn.prepareStatement("SELECT `ap_id`,`id` FROM `group_memberships` INNER JOIN `users` ON `users`.`id`=`user_id` WHERE `group_id`=? AND `accepted`=1 AND tentative=? LIMIT ? OFFSET ?"); stmt.setInt(1, groupID); @@ -531,9 +553,9 @@ public static List getGroupMemberURIs(int groupID, boolean tentative, int o }while(res.next()); } } - return list; + return new PaginatedList<>(list, total, offset, count); } - return Collections.emptyList(); + return new PaginatedList<>(Collections.emptyList(), total, offset, count); } public static List getGroupMemberInboxes(int groupID) throws SQLException{ diff --git a/src/main/resources/langs/en.json b/src/main/resources/langs/en.json index a2dc439e..e6fbe8db 100644 --- a/src/main/resources/langs/en.json +++ b/src/main/resources/langs/en.json @@ -475,5 +475,14 @@ "feed_created_group_before": " created the group ", "feed_created_group_after": ".", "feed_created_event_before": " created the event ", - "feed_created_event_after": "." + "feed_created_event_after": ".", + "join_event_certain": "I'll be there", + "join_event_tentative": "I might go", + "event_joined_certain": "You'll be there.", + "event_joined_tentative": "You aren't sure.", + "X_people": "{count, plural, one {One person} other {# people}}", + "tentative_members": "Not sure", + "X_tentative_members": "{count} not sure", + "summary_event_X_members": "{count, plural, one {One person is} other {# people are}} attending", + "summary_event_X_tentative_members": "{count, plural, one {One person is} other {# people are}} not sure" } \ No newline at end of file diff --git a/src/main/resources/langs/ru.json b/src/main/resources/langs/ru.json index c098abe9..385efe00 100644 --- a/src/main/resources/langs/ru.json +++ b/src/main/resources/langs/ru.json @@ -464,7 +464,7 @@ "no_groups": "Нет ни одной группы", "no_events": "Нет ни одной встречи", "events": "Встречи", - "summary_X_upcoming_events": "{count, select, 0 {У Вас нет предстоящих встреч} other {Вы пойдёте на {count, plural, one {# встречу} other {# встреч}}}}", + "summary_X_upcoming_events": "{count, select, 0 {У Вас нет предстоящих встреч} other {Вы пойдёте на {count, plural, one {# встречу} few {# встречи} other {# встреч}}}}", "summary_X_past_events": "У Вас {count, plural, =0 {нет прошедших встреч} one {# прошедшая встреча} few {# прошедшие встречи} other {# прошедших встреч}}", "date_time_separator": "в", "write_on_event_wall": "Написать на стене встречи...", @@ -477,5 +477,14 @@ "feed_created_group_before": " {gender, select, female {создала} other {создал}} группу ", "feed_created_group_after": ".", "feed_created_event_before": " {gender, select, female {создала} other {создал}} встречу ", - "feed_created_event_after": "." + "feed_created_event_after": ".", + "join_event_certain": "Точно пойду", + "join_event_tentative": "Возможно пойду", + "event_joined_certain": "Вы пойдёте.", + "event_joined_tentative": "Вы не уверены, что пойдёте.", + "X_people": "{count, plural, one {# человек} few {# человека} other {# человек}}", + "tentative_members": "Возможные участники", + "X_tentative_members": "{count, plural, one {# не уверен} other {# не уверены}}", + "summary_event_X_members": "{count, plural, one {# человек пойдёт} few {# человека пойдут} other {# человек пойдут}} на встречу", + "summary_event_X_tentative_members": "{count, plural, one {# человек не уверен, что придёт} few {# человека не уверены, что придут} other {# человек не уверены, что придут}}" } \ No newline at end of file diff --git a/src/main/resources/templates/desktop/group.twig b/src/main/resources/templates/desktop/group.twig index f2f30323..407664aa 100644 --- a/src/main/resources/templates/desktop/group.twig +++ b/src/main/resources/templates/desktop/group.twig @@ -59,14 +59,30 @@ {% if currentUser is not null and (membershipStateText is not empty or membershipState=="NONE") %}
    {{ membershipStateText }} + {% if group.event %} + {% if membershipState=="NONE" %} + {{ L('join_event_certain') }} + {% if group.domain is empty or group.hasCapability("TENTATIVE_MEMBERSHIP") %} + {{ L('join_event_tentative') }} + {% endif %} + {% endif %} + {% else %} {% if membershipState=="NONE" %} {{ L('join_group') }} {% endif %} + {% endif %}
    {% endif %}
      {% if currentUser is not null %} - {% if membershipState=="MEMBER" %} + {% if membershipState=="MEMBER" or membershipState=="TENTATIVE_MEMBER" %} + {% if group.event and (group.domain is empty or group.hasCapability("TENTATIVE_MEMBERSHIP")) %} + {% if membershipState=="MEMBER" %} +
    • {{ L('join_event_tentative') }}
    • + {% else %} +
    • {{ L('join_event_certain') }}
    • + {% endif %} + {% endif %}
    • {{ L(group.event ? 'leave_event' : 'leave_group') }}
    • {% endif %} {% endif %} @@ -79,7 +95,10 @@
      - {% include "profile_module_user_grid" with {'users': members, 'headerTitle': L('members'), 'headerHref': "/groups/#{group.id}/members", 'subheaderTitle': L('X_members', {'count': group.memberCount})} %} + {% include "profile_module_user_grid" with {'users': members, 'headerTitle': L('members'), 'headerHref': "/groups/#{group.id}/members", 'subheaderTitle': L('X_people', {'count': group.memberCount})} %} + {% if group.event and group.tentativeMemberCount>0 %} + {% include "profile_module_user_grid" with {'users': tentativeMembers, 'headerTitle': L('tentative_members'), 'headerHref': "/groups/#{group.id}/tentativeMembers", 'subheaderTitle': L('X_people', {'count': group.tentativeMemberCount})} %} + {% endif %} {% include "profile_module_user_list" with {'users': admins, 'headerTitle': L(group.event ? 'event_organizers' : 'group_admins'), 'headerHref': "/groups/#{group.id}/admins", 'headerAjaxBox': true, 'subheaderTitle': L(group.event ? 'event_X_organizers' : 'group_X_admins', {'count': admins.size})} %}
      diff --git a/src/main/resources/templates/mobile/group.twig b/src/main/resources/templates/mobile/group.twig index 7d3aa991..10fe3040 100644 --- a/src/main/resources/templates/mobile/group.twig +++ b/src/main/resources/templates/mobile/group.twig @@ -27,9 +27,18 @@ {% if currentUser is not null and (membershipStateText is not empty or membershipState=="NONE") %}
      {{ membershipStateText }} + {% if group.event %} + {% if membershipState=="NONE" %} + {{ L('join_event_certain') }} + {% if group.domain is empty or group.hasCapability("TENTATIVE_MEMBERSHIP") %} + {{ L('join_event_tentative') }} + {% endif %} + {% endif %} + {% else %} {% if membershipState=="NONE" %} {{ L('join_group') }} {% endif %} + {% endif %}
      {% endif %}
        @@ -43,10 +52,20 @@
      • {%endif%}
      • {{ L('X_members', {'count': group.memberCount}) }}
      • -
      • {{ L('group_X_admins', {'count': admins.size}) }}
      • + {% if group.tentativeMemberCount>0 %} +
      • {{ L('X_tentative_members', {'count': group.tentativeMemberCount}) }}
      • + {% endif %} +
      • {{ L(group.event ? 'event_X_organizers' : 'group_X_admins', {'count': admins.size}) }}
      • {% if currentUser is not null %} + {% if membershipState=="MEMBER" or membershipState=="TENTATIVE_MEMBER" %} + {% if group.event and (group.domain is empty or group.hasCapability("TENTATIVE_MEMBERSHIP")) %} {% if membershipState=="MEMBER" %} +
      • {{ L('join_event_tentative') }}
      • + {% else %} +
      • {{ L('join_event_certain') }}
      • + {% endif %} + {% endif %}
      • {{ L(group.event ? 'leave_event' : 'leave_group') }}
      • {% endif %} {% endif %} diff --git a/src/main/web/mobile.scss b/src/main/web/mobile.scss index d94fa853..fbd2233a 100644 --- a/src/main/web/mobile.scss +++ b/src/main/web/mobile.scss @@ -651,9 +651,11 @@ select{ text-align: center; padding: 16px; .withText{ - width: 100%; margin-top: 16px; } + .button{ + width: 100%; + } } .profileFields{ From 35f179e435f9d44214f99733184b75093870efd8 Mon Sep 17 00:00:00 2001 From: Grishka Date: Tue, 21 Dec 2021 00:46:02 +0300 Subject: [PATCH 16/65] Fixes --- src/main/java/smithereen/routes/WellKnownRoutes.java | 6 +++--- src/main/java/smithereen/storage/PostStorage.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/smithereen/routes/WellKnownRoutes.java b/src/main/java/smithereen/routes/WellKnownRoutes.java index baaa0e9a..34d764f6 100644 --- a/src/main/java/smithereen/routes/WellKnownRoutes.java +++ b/src/main/java/smithereen/routes/WellKnownRoutes.java @@ -33,7 +33,7 @@ public static Object webfinger(Request req, Response resp) throws SQLException{ wfr.subject="acct:"+username+"@"+Config.domain; WebfingerResponse.Link selfLink=new WebfingerResponse.Link(); selfLink.rel="self"; - selfLink.type="application/activitypub+json"; + selfLink.type="application/activity+json"; selfLink.href=Config.localURI("/activitypub/serviceActor"); wfr.links=List.of(selfLink); return Utils.gson.toJson(wfr); @@ -46,7 +46,7 @@ public static Object webfinger(Request req, Response resp) throws SQLException{ wfr.subject="acct:"+user.username+"@"+Config.domain; WebfingerResponse.Link selfLink=new WebfingerResponse.Link(); selfLink.rel="self"; - selfLink.type="application/activitypub+json"; + selfLink.type="application/activity+json"; selfLink.href=user.activityPubID; WebfingerResponse.Link authLink=new WebfingerResponse.Link(); authLink.rel="http://ostatus.org/schema/1.0/subscribe"; @@ -62,7 +62,7 @@ public static Object webfinger(Request req, Response resp) throws SQLException{ wfr.subject="acct:"+group.username+"@"+Config.domain; WebfingerResponse.Link selfLink=new WebfingerResponse.Link(); selfLink.rel="self"; - selfLink.type="application/activitypub+json"; + selfLink.type="application/activity+json"; selfLink.href=group.activityPubID; wfr.links=List.of(selfLink); return Utils.gson.toJson(wfr); diff --git a/src/main/java/smithereen/storage/PostStorage.java b/src/main/java/smithereen/storage/PostStorage.java index e8657cef..4170d716 100644 --- a/src/main/java/smithereen/storage/PostStorage.java +++ b/src/main/java/smithereen/storage/PostStorage.java @@ -564,7 +564,7 @@ public static Map> getRepliesForFeed(Set p if(postIDs.isEmpty()) return Collections.emptyMap(); Connection conn=DatabaseConnectionManager.getConnection(); - PreparedStatement stmt=conn.prepareStatement(String.join(" UNION ALL ", Collections.nCopies(postIDs.size(), "(SELECT * FROM wall_posts WHERE reply_key=? ORDER BY id DESC LIMIT 3)"))); + PreparedStatement stmt=conn.prepareStatement(String.join(" UNION ALL ", Collections.nCopies(postIDs.size(), "(SELECT * FROM wall_posts WHERE reply_key=? ORDER BY created_at DESC LIMIT 3)"))); int i=0; for(int id:postIDs){ stmt.setBytes(i+1, Utils.serializeIntArray(new int[]{id})); From 9e0fea24d07613a599ea77319596d9ebcd6dd4a6 Mon Sep 17 00:00:00 2001 From: Grishka Date: Thu, 23 Dec 2021 16:11:32 +0300 Subject: [PATCH 17/65] Update Russian inflection rules --- codegen/gen_inflection_rules.php | 40 +- codegen/gender_ru.json | 11 +- codegen/inflect_ru.json | 33 +- .../lang/RussianInflectionRulesGenerated.java | 1185 ++++++----------- 4 files changed, 511 insertions(+), 758 deletions(-) diff --git a/codegen/gen_inflection_rules.php b/codegen/gen_inflection_rules.php index dda443c1..0c216002 100644 --- a/codegen/gen_inflection_rules.php +++ b/codegen/gen_inflection_rules.php @@ -21,22 +21,43 @@ function generateCases($cases){ $j[]="\t\t\treturn input;"; return; } - $j[]="\t\t\tswitch(_case){"; + $removeCounts=[]; for($i=0;$i0) + $j[]="\t\t\treturn input.substring(0, input.length()-$commonRemoveCount)+switch(_case){"; + elseif($commonRemoveCount==0) + $j[]="\t\t\treturn input+switch(_case){"; + else + $j[]="\t\t\treturn switch(_case){"; + for($i=0;$i{$key}->suffixes; $j[]="\tpublic static String $method(String input, User.Gender gender, Inflector.Case _case, boolean firstWord){"; + $j[]="\t\tif(_case==Inflector.Case.NOMINATIVE) return input;"; $j[]="\t\tString inputLower=input.toLowerCase();"; foreach($all as $gk=>$group){ foreach ($group as $ex) { diff --git a/codegen/gender_ru.json b/codegen/gender_ru.json index 9d152388..8efd0c56 100644 --- a/codegen/gender_ru.json +++ b/codegen/gender_ru.json @@ -51,7 +51,8 @@ "райхон", "закия", "захария", - "женя" + "женя", + "карен" ], "male": [ "абиба", @@ -189,7 +190,8 @@ "костя", "алья", "илья", - "ларья" + "ларья", + "артём" ], "female": [ "судаба", @@ -416,10 +418,7 @@ "ым", "ям", "ан", - "бен", - "вен", - "ген", - "ден", + "ен", "ин", "сейн", "он", diff --git a/codegen/inflect_ru.json b/codegen/inflect_ru.json index 72cfedb2..eaecf38e 100644 --- a/codegen/inflect_ru.json +++ b/codegen/inflect_ru.json @@ -65,7 +65,8 @@ "долгопалец", "маненок", "рева", - "кива" + "кива", + "щёлок" ], "mods": [ ".", @@ -464,6 +465,7 @@ "gender": "male", "test": [ "нец", + "мец", "робец" ], "mods": [ @@ -518,6 +520,7 @@ "gender": "male", "test": [ "ах", + "ав", "ив", "шток" ], @@ -633,6 +636,7 @@ "gender": "male", "test": [ "вец", + "сец", "убец", "ырец" ], @@ -1054,6 +1058,33 @@ "-е" ] }, + { + "gender": "male", + "test": [ + "бек" + ], + "mods": [ + "-ка", + "-ку", + "-ка", + "-ком", + "-ке" + ] + }, + { + "gender": "male", + "test": [ + "ек", + "ёк" + ], + "mods": [ + "--ька", + "--ьку", + "--ька", + "--ьком", + "--ьке" + ] + }, { "gender": "male", "test": [ diff --git a/src/main/java/smithereen/lang/RussianInflectionRulesGenerated.java b/src/main/java/smithereen/lang/RussianInflectionRulesGenerated.java index 141b644e..6767f9b2 100644 --- a/src/main/java/smithereen/lang/RussianInflectionRulesGenerated.java +++ b/src/main/java/smithereen/lang/RussianInflectionRulesGenerated.java @@ -8,6 +8,7 @@ class RussianInflectionRulesGenerated{ public static String inflectLastName(String input, User.Gender gender, Inflector.Case _case, boolean firstWord){ + if(_case==Inflector.Case.NOMINATIVE) return input; String inputLower=input.toLowerCase(); if(firstWord && (inputLower.equals("бонч") || inputLower.equals("абдул") || inputLower.equals("белиц") || inputLower.equals("гасан") || inputLower.equals("дюссар") || inputLower.equals("дюмон") || inputLower.equals("книппер") || inputLower.equals("корвин") || inputLower.equals("ван") || inputLower.equals("шолом") || inputLower.equals("тер") || inputLower.equals("призван") || inputLower.equals("мелик") || inputLower.equals("вар") || inputLower.equals("фон"))){ return input; @@ -15,36 +16,26 @@ public static String inflectLastName(String input, User.Gender gender, Inflector if(inputLower.equals("дюма") || inputLower.equals("тома") || inputLower.equals("дега") || inputLower.equals("люка") || inputLower.equals("ферма") || inputLower.equals("гамарра") || inputLower.equals("петипа") || inputLower.equals("шандра") || inputLower.equals("скаля") || inputLower.equals("каруана")){ return input; } - if(inputLower.equals("гусь") || inputLower.equals("ремень") || inputLower.equals("камень") || inputLower.equals("онук") || inputLower.equals("богода") || inputLower.equals("нечипас") || inputLower.equals("долгопалец") || inputLower.equals("маненок") || inputLower.equals("рева") || inputLower.equals("кива")){ + if(inputLower.equals("гусь") || inputLower.equals("ремень") || inputLower.equals("камень") || inputLower.equals("онук") || inputLower.equals("богода") || inputLower.equals("нечипас") || inputLower.equals("долгопалец") || inputLower.equals("маненок") || inputLower.equals("рева") || inputLower.equals("кива") || inputLower.equals("щёлок")){ return input; } if(gender==User.Gender.MALE && (inputLower.equals("вий") || inputLower.equals("сой") || inputLower.equals("цой") || inputLower.equals("хой"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"я"; - case DATIVE: - return input.substring(0, input.length()-1)+"ю"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"я"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ем"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "я"; + case DATIVE -> "ю"; + case INSTRUMENTAL -> "ем"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.equals("грин") || inputLower.equals("дарвин") || inputLower.equals("регин") || inputLower.equals("цин"))){ - switch(_case){ - case GENITIVE: - return input+"а"; - case DATIVE: - return input+"у"; - case ACCUSATIVE: - return input+"а"; - case INSTRUMENTAL: - return input+"ом"; - case PREPOSITIONAL: - return input+"е"; - } + return input+switch(_case){ + case GENITIVE, ACCUSATIVE -> "а"; + case DATIVE -> "у"; + case INSTRUMENTAL -> "ом"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.FEMALE && (inputLower.endsWith("б") || inputLower.endsWith("в") || inputLower.endsWith("г") || inputLower.endsWith("д") || inputLower.endsWith("ж") || inputLower.endsWith("з") || inputLower.endsWith("й") || inputLower.endsWith("к") || inputLower.endsWith("л") || inputLower.endsWith("м") || inputLower.endsWith("н") || inputLower.endsWith("п") || inputLower.endsWith("р") || inputLower.endsWith("с") || inputLower.endsWith("т") || inputLower.endsWith("ф") || inputLower.endsWith("х") || inputLower.endsWith("ц") || inputLower.endsWith("ч") || inputLower.endsWith("ш") || inputLower.endsWith("щ") || inputLower.endsWith("ъ") || inputLower.endsWith("ь"))){ return input; @@ -53,130 +44,76 @@ public static String inflectLastName(String input, User.Gender gender, Inflector return input; } if(gender==User.Gender.FEMALE && (inputLower.endsWith("ска") || inputLower.endsWith("цка"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"ой"; - case DATIVE: - return input.substring(0, input.length()-1)+"ой"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"ую"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ой"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"ой"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE, PREPOSITIONAL, INSTRUMENTAL, DATIVE -> "ой"; + case ACCUSATIVE -> "ую"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.FEMALE && (inputLower.endsWith("чая"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-2)+"ей"; - case DATIVE: - return input.substring(0, input.length()-2)+"ей"; - case ACCUSATIVE: - return input.substring(0, input.length()-2)+"ую"; - case INSTRUMENTAL: - return input.substring(0, input.length()-2)+"ей"; - case PREPOSITIONAL: - return input.substring(0, input.length()-2)+"ей"; - } + return input.substring(0, input.length()-2)+switch(_case){ + case GENITIVE, PREPOSITIONAL, INSTRUMENTAL, DATIVE -> "ей"; + case ACCUSATIVE -> "ую"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("чий"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-2)+"его"; - case DATIVE: - return input.substring(0, input.length()-2)+"ему"; - case ACCUSATIVE: - return input.substring(0, input.length()-2)+"его"; - case INSTRUMENTAL: - return input.substring(0, input.length()-2)+"им"; - case PREPOSITIONAL: - return input.substring(0, input.length()-2)+"ем"; - } + return input.substring(0, input.length()-2)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "его"; + case DATIVE -> "ему"; + case INSTRUMENTAL -> "им"; + case PREPOSITIONAL -> "ем"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.FEMALE && (inputLower.endsWith("цкая") || inputLower.endsWith("ская") || inputLower.endsWith("ная") || inputLower.endsWith("ая"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-2)+"ой"; - case DATIVE: - return input.substring(0, input.length()-2)+"ой"; - case ACCUSATIVE: - return input.substring(0, input.length()-2)+"ую"; - case INSTRUMENTAL: - return input.substring(0, input.length()-2)+"ой"; - case PREPOSITIONAL: - return input.substring(0, input.length()-2)+"ой"; - } + return input.substring(0, input.length()-2)+switch(_case){ + case GENITIVE, PREPOSITIONAL, INSTRUMENTAL, DATIVE -> "ой"; + case ACCUSATIVE -> "ую"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.FEMALE && (inputLower.endsWith("яя"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-2)+"ей"; - case DATIVE: - return input.substring(0, input.length()-2)+"ей"; - case ACCUSATIVE: - return input.substring(0, input.length()-2)+"юю"; - case INSTRUMENTAL: - return input.substring(0, input.length()-2)+"ей"; - case PREPOSITIONAL: - return input.substring(0, input.length()-2)+"ей"; - } + return input.substring(0, input.length()-2)+switch(_case){ + case GENITIVE, PREPOSITIONAL, INSTRUMENTAL, DATIVE -> "ей"; + case ACCUSATIVE -> "юю"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("иной") || inputLower.endsWith("уй"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"я"; - case DATIVE: - return input.substring(0, input.length()-1)+"ю"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"я"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ем"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "я"; + case DATIVE -> "ю"; + case INSTRUMENTAL -> "ем"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(inputLower.endsWith("ца")){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"ы"; - case DATIVE: - return input.substring(0, input.length()-1)+"е"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"у"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ей"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE -> "ы"; + case DATIVE, PREPOSITIONAL -> "е"; + case ACCUSATIVE -> "у"; + case INSTRUMENTAL -> "ей"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("рих"))){ - switch(_case){ - case GENITIVE: - return input+"а"; - case DATIVE: - return input+"у"; - case ACCUSATIVE: - return input+"а"; - case INSTRUMENTAL: - return input+"ом"; - case PREPOSITIONAL: - return input+"е"; - } + return input+switch(_case){ + case GENITIVE, ACCUSATIVE -> "а"; + case DATIVE -> "у"; + case INSTRUMENTAL -> "ом"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(inputLower.endsWith("ия")){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"и"; - case DATIVE: - return input.substring(0, input.length()-1)+"и"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"ю"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ей"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"и"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE, PREPOSITIONAL, DATIVE -> "и"; + case ACCUSATIVE -> "ю"; + case INSTRUMENTAL -> "ей"; + default -> throw new IllegalArgumentException(); + }; } if(inputLower.endsWith("иа") || inputLower.endsWith("аа") || inputLower.endsWith("оа") || inputLower.endsWith("уа") || inputLower.endsWith("ыа") || inputLower.endsWith("еа") || inputLower.endsWith("юа") || inputLower.endsWith("эа")){ return input; @@ -188,446 +125,289 @@ public static String inflectLastName(String input, User.Gender gender, Inflector return input; } if(gender==User.Gender.FEMALE && (inputLower.endsWith("ова") || inputLower.endsWith("ева") || inputLower.endsWith("на") || inputLower.endsWith("ёва"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"ой"; - case DATIVE: - return input.substring(0, input.length()-1)+"ой"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"у"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ой"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"ой"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE, PREPOSITIONAL, INSTRUMENTAL, DATIVE -> "ой"; + case ACCUSATIVE -> "у"; + default -> throw new IllegalArgumentException(); + }; } if(inputLower.endsWith("га") || inputLower.endsWith("ка") || inputLower.endsWith("ха") || inputLower.endsWith("ча") || inputLower.endsWith("ща") || inputLower.endsWith("жа") || inputLower.endsWith("ша")){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"и"; - case DATIVE: - return input.substring(0, input.length()-1)+"е"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"у"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ой"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE -> "и"; + case DATIVE, PREPOSITIONAL -> "е"; + case ACCUSATIVE -> "у"; + case INSTRUMENTAL -> "ой"; + default -> throw new IllegalArgumentException(); + }; } if(inputLower.endsWith("а")){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"ы"; - case DATIVE: - return input.substring(0, input.length()-1)+"е"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"у"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ой"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE -> "ы"; + case DATIVE, PREPOSITIONAL -> "е"; + case ACCUSATIVE -> "у"; + case INSTRUMENTAL -> "ой"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ь"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"я"; - case DATIVE: - return input.substring(0, input.length()-1)+"ю"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"я"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ем"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "я"; + case DATIVE -> "ю"; + case INSTRUMENTAL -> "ем"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(inputLower.endsWith("я")){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"и"; - case DATIVE: - return input.substring(0, input.length()-1)+"е"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"ю"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ей"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE -> "и"; + case DATIVE, PREPOSITIONAL -> "е"; + case ACCUSATIVE -> "ю"; + case INSTRUMENTAL -> "ей"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("обей"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-2)+"ья"; - case DATIVE: - return input.substring(0, input.length()-2)+"ью"; - case ACCUSATIVE: - return input.substring(0, input.length()-2)+"ья"; - case INSTRUMENTAL: - return input.substring(0, input.length()-2)+"ьем"; - case PREPOSITIONAL: - return input.substring(0, input.length()-2)+"ье"; - } + return input.substring(0, input.length()-2)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "ья"; + case DATIVE -> "ью"; + case INSTRUMENTAL -> "ьем"; + case PREPOSITIONAL -> "ье"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ей"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"я"; - case DATIVE: - return input.substring(0, input.length()-1)+"ю"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"я"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ем"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "я"; + case DATIVE -> "ю"; + case INSTRUMENTAL -> "ем"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ян") || inputLower.endsWith("ан") || inputLower.endsWith("йн"))){ - switch(_case){ - case GENITIVE: - return input+"а"; - case DATIVE: - return input+"у"; - case ACCUSATIVE: - return input+"а"; - case INSTRUMENTAL: - return input+"ом"; - case PREPOSITIONAL: - return input+"е"; - } + return input+switch(_case){ + case GENITIVE, ACCUSATIVE -> "а"; + case DATIVE -> "у"; + case INSTRUMENTAL -> "ом"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ынец"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-2)+"ца"; - case DATIVE: - return input.substring(0, input.length()-2)+"цу"; - case ACCUSATIVE: - return input.substring(0, input.length()-2)+"ца"; - case INSTRUMENTAL: - return input.substring(0, input.length()-2)+"цом"; - case PREPOSITIONAL: - return input.substring(0, input.length()-2)+"це"; - } - } - if(gender==User.Gender.MALE && (inputLower.endsWith("нец") || inputLower.endsWith("робец"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-2)+"ца"; - case DATIVE: - return input.substring(0, input.length()-2)+"цу"; - case ACCUSATIVE: - return input.substring(0, input.length()-2)+"ца"; - case INSTRUMENTAL: - return input.substring(0, input.length()-2)+"цем"; - case PREPOSITIONAL: - return input.substring(0, input.length()-2)+"це"; - } + return input.substring(0, input.length()-2)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "ца"; + case DATIVE -> "цу"; + case INSTRUMENTAL -> "цом"; + case PREPOSITIONAL -> "це"; + default -> throw new IllegalArgumentException(); + }; + } + if(gender==User.Gender.MALE && (inputLower.endsWith("нец") || inputLower.endsWith("мец") || inputLower.endsWith("робец"))){ + return input.substring(0, input.length()-2)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "ца"; + case DATIVE -> "цу"; + case INSTRUMENTAL -> "цем"; + case PREPOSITIONAL -> "це"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ай"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"я"; - case DATIVE: - return input.substring(0, input.length()-1)+"ю"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"я"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ем"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "я"; + case DATIVE -> "ю"; + case INSTRUMENTAL -> "ем"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("гой") || inputLower.endsWith("кой"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"го"; - case DATIVE: - return input.substring(0, input.length()-1)+"му"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"го"; - case INSTRUMENTAL: - return input.substring(0, input.length()-2)+"им"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"м"; - } + return switch(_case){ + case GENITIVE, ACCUSATIVE -> input.substring(0, input.length()-1)+"го"; + case DATIVE -> input.substring(0, input.length()-1)+"му"; + case INSTRUMENTAL -> input.substring(0, input.length()-2)+"им"; + case PREPOSITIONAL -> input.substring(0, input.length()-1)+"м"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ой"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"го"; - case DATIVE: - return input.substring(0, input.length()-1)+"му"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"го"; - case INSTRUMENTAL: - return input.substring(0, input.length()-2)+"ым"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"м"; - } - } - if(gender==User.Gender.MALE && (inputLower.endsWith("ах") || inputLower.endsWith("ив") || inputLower.endsWith("шток"))){ - switch(_case){ - case GENITIVE: - return input+"а"; - case DATIVE: - return input+"у"; - case ACCUSATIVE: - return input+"а"; - case INSTRUMENTAL: - return input+"ом"; - case PREPOSITIONAL: - return input+"е"; - } + return switch(_case){ + case GENITIVE, ACCUSATIVE -> input.substring(0, input.length()-1)+"го"; + case DATIVE -> input.substring(0, input.length()-1)+"му"; + case INSTRUMENTAL -> input.substring(0, input.length()-2)+"ым"; + case PREPOSITIONAL -> input.substring(0, input.length()-1)+"м"; + default -> throw new IllegalArgumentException(); + }; + } + if(gender==User.Gender.MALE && (inputLower.endsWith("ах") || inputLower.endsWith("ав") || inputLower.endsWith("ив") || inputLower.endsWith("шток"))){ + return input+switch(_case){ + case GENITIVE, ACCUSATIVE -> "а"; + case DATIVE -> "у"; + case INSTRUMENTAL -> "ом"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ший") || inputLower.endsWith("щий") || inputLower.endsWith("жий") || inputLower.endsWith("ний"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-2)+"его"; - case DATIVE: - return input.substring(0, input.length()-2)+"ему"; - case ACCUSATIVE: - return input.substring(0, input.length()-2)+"его"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"м"; - case PREPOSITIONAL: - return input.substring(0, input.length()-2)+"ем"; - } + return switch(_case){ + case GENITIVE, ACCUSATIVE -> input.substring(0, input.length()-2)+"его"; + case DATIVE -> input.substring(0, input.length()-2)+"ему"; + case INSTRUMENTAL -> input.substring(0, input.length()-1)+"м"; + case PREPOSITIONAL -> input.substring(0, input.length()-2)+"ем"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ый") || inputLower.endsWith("кий") || inputLower.endsWith("хий"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-2)+"ого"; - case DATIVE: - return input.substring(0, input.length()-2)+"ому"; - case ACCUSATIVE: - return input.substring(0, input.length()-2)+"ого"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"м"; - case PREPOSITIONAL: - return input.substring(0, input.length()-2)+"ом"; - } + return switch(_case){ + case GENITIVE, ACCUSATIVE -> input.substring(0, input.length()-2)+"ого"; + case DATIVE -> input.substring(0, input.length()-2)+"ому"; + case INSTRUMENTAL -> input.substring(0, input.length()-1)+"м"; + case PREPOSITIONAL -> input.substring(0, input.length()-2)+"ом"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ий"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"я"; - case DATIVE: - return input.substring(0, input.length()-1)+"ю"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"я"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ем"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"и"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "я"; + case DATIVE -> "ю"; + case INSTRUMENTAL -> "ем"; + case PREPOSITIONAL -> "и"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ок"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-2)+"ка"; - case DATIVE: - return input.substring(0, input.length()-2)+"ку"; - case ACCUSATIVE: - return input.substring(0, input.length()-2)+"ка"; - case INSTRUMENTAL: - return input.substring(0, input.length()-2)+"ком"; - case PREPOSITIONAL: - return input.substring(0, input.length()-2)+"ке"; - } + return input.substring(0, input.length()-2)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "ка"; + case DATIVE -> "ку"; + case INSTRUMENTAL -> "ком"; + case PREPOSITIONAL -> "ке"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("обец") || inputLower.endsWith("швец") || inputLower.endsWith("ьвец"))){ - switch(_case){ - case GENITIVE: - return input+"а"; - case DATIVE: - return input+"у"; - case ACCUSATIVE: - return input+"а"; - case INSTRUMENTAL: - return input+"ем"; - case PREPOSITIONAL: - return input+"е"; - } + return input+switch(_case){ + case GENITIVE, ACCUSATIVE -> "а"; + case DATIVE -> "у"; + case INSTRUMENTAL -> "ем"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("аец") || inputLower.endsWith("иец") || inputLower.endsWith("еец"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-2)+"йца"; - case DATIVE: - return input.substring(0, input.length()-2)+"йцу"; - case ACCUSATIVE: - return input.substring(0, input.length()-2)+"йца"; - case INSTRUMENTAL: - return input.substring(0, input.length()-2)+"йцем"; - case PREPOSITIONAL: - return input.substring(0, input.length()-2)+"йце"; - } + return input.substring(0, input.length()-2)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "йца"; + case DATIVE -> "йцу"; + case INSTRUMENTAL -> "йцем"; + case PREPOSITIONAL -> "йце"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("опец"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-2)+"ца"; - case DATIVE: - return input.substring(0, input.length()-2)+"цу"; - case ACCUSATIVE: - return input.substring(0, input.length()-2)+"ца"; - case INSTRUMENTAL: - return input.substring(0, input.length()-2)+"цем"; - case PREPOSITIONAL: - return input.substring(0, input.length()-2)+"це"; - } - } - if(gender==User.Gender.MALE && (inputLower.endsWith("вец") || inputLower.endsWith("убец") || inputLower.endsWith("ырец"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-2)+"ца"; - case DATIVE: - return input.substring(0, input.length()-2)+"цу"; - case ACCUSATIVE: - return input.substring(0, input.length()-2)+"ца"; - case INSTRUMENTAL: - return input.substring(0, input.length()-2)+"цом"; - case PREPOSITIONAL: - return input.substring(0, input.length()-2)+"це"; - } + return input.substring(0, input.length()-2)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "ца"; + case DATIVE -> "цу"; + case INSTRUMENTAL -> "цем"; + case PREPOSITIONAL -> "це"; + default -> throw new IllegalArgumentException(); + }; + } + if(gender==User.Gender.MALE && (inputLower.endsWith("вец") || inputLower.endsWith("сец") || inputLower.endsWith("убец") || inputLower.endsWith("ырец"))){ + return input.substring(0, input.length()-2)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "ца"; + case DATIVE -> "цу"; + case INSTRUMENTAL -> "цом"; + case PREPOSITIONAL -> "це"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ц") || inputLower.endsWith("ч") || inputLower.endsWith("ш") || inputLower.endsWith("щ"))){ - switch(_case){ - case GENITIVE: - return input+"а"; - case DATIVE: - return input+"у"; - case ACCUSATIVE: - return input+"а"; - case INSTRUMENTAL: - return input+"ем"; - case PREPOSITIONAL: - return input+"е"; - } + return input+switch(_case){ + case GENITIVE, ACCUSATIVE -> "а"; + case DATIVE -> "у"; + case INSTRUMENTAL -> "ем"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ен") || inputLower.endsWith("нн") || inputLower.endsWith("он") || inputLower.endsWith("ун") || inputLower.endsWith("б") || inputLower.endsWith("г") || inputLower.endsWith("д") || inputLower.endsWith("ж") || inputLower.endsWith("з") || inputLower.endsWith("к") || inputLower.endsWith("л") || inputLower.endsWith("м") || inputLower.endsWith("п") || inputLower.endsWith("р") || inputLower.endsWith("с") || inputLower.endsWith("т") || inputLower.endsWith("ф") || inputLower.endsWith("х"))){ - switch(_case){ - case GENITIVE: - return input+"а"; - case DATIVE: - return input+"у"; - case ACCUSATIVE: - return input+"а"; - case INSTRUMENTAL: - return input+"ом"; - case PREPOSITIONAL: - return input+"е"; - } + return input+switch(_case){ + case GENITIVE, ACCUSATIVE -> "а"; + case DATIVE -> "у"; + case INSTRUMENTAL -> "ом"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("в") || inputLower.endsWith("н"))){ - switch(_case){ - case GENITIVE: - return input+"а"; - case DATIVE: - return input+"у"; - case ACCUSATIVE: - return input+"а"; - case INSTRUMENTAL: - return input+"ым"; - case PREPOSITIONAL: - return input+"е"; - } + return input+switch(_case){ + case GENITIVE, ACCUSATIVE -> "а"; + case DATIVE -> "у"; + case INSTRUMENTAL -> "ым"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } return input; } public static String inflectFirstName(String input, User.Gender gender, Inflector.Case _case, boolean firstWord){ + if(_case==Inflector.Case.NOMINATIVE) return input; String inputLower=input.toLowerCase(); if(gender==User.Gender.MALE && (inputLower.equals("лев"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-2)+"ьва"; - case DATIVE: - return input.substring(0, input.length()-2)+"ьву"; - case ACCUSATIVE: - return input.substring(0, input.length()-2)+"ьва"; - case INSTRUMENTAL: - return input.substring(0, input.length()-2)+"ьвом"; - case PREPOSITIONAL: - return input.substring(0, input.length()-2)+"ьве"; - } + return input.substring(0, input.length()-2)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "ьва"; + case DATIVE -> "ьву"; + case INSTRUMENTAL -> "ьвом"; + case PREPOSITIONAL -> "ьве"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.equals("пётр"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-3)+"етра"; - case DATIVE: - return input.substring(0, input.length()-3)+"етру"; - case ACCUSATIVE: - return input.substring(0, input.length()-3)+"етра"; - case INSTRUMENTAL: - return input.substring(0, input.length()-3)+"етром"; - case PREPOSITIONAL: - return input.substring(0, input.length()-3)+"етре"; - } + return input.substring(0, input.length()-3)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "етра"; + case DATIVE -> "етру"; + case INSTRUMENTAL -> "етром"; + case PREPOSITIONAL -> "етре"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.equals("павел"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-2)+"ла"; - case DATIVE: - return input.substring(0, input.length()-2)+"лу"; - case ACCUSATIVE: - return input.substring(0, input.length()-2)+"ла"; - case INSTRUMENTAL: - return input.substring(0, input.length()-2)+"лом"; - case PREPOSITIONAL: - return input.substring(0, input.length()-2)+"ле"; - } + return input.substring(0, input.length()-2)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "ла"; + case DATIVE -> "лу"; + case INSTRUMENTAL -> "лом"; + case PREPOSITIONAL -> "ле"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.equals("яша"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"и"; - case DATIVE: - return input.substring(0, input.length()-1)+"е"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"у"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ей"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE -> "и"; + case DATIVE, PREPOSITIONAL -> "е"; + case ACCUSATIVE -> "у"; + case INSTRUMENTAL -> "ей"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.equals("илья"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"и"; - case DATIVE: - return input.substring(0, input.length()-1)+"е"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"ю"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ёй"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE -> "и"; + case DATIVE, PREPOSITIONAL -> "е"; + case ACCUSATIVE -> "ю"; + case INSTRUMENTAL -> "ёй"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.equals("шота"))){ return input; } if(gender==User.Gender.FEMALE && (inputLower.equals("агидель") || inputLower.equals("жизель") || inputLower.equals("нинель") || inputLower.equals("рашель") || inputLower.equals("рахиль"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"и"; - case DATIVE: - return input.substring(0, input.length()-1)+"и"; - case ACCUSATIVE: - return input; - case INSTRUMENTAL: - return input+"ю"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"и"; - } + return switch(_case){ + case GENITIVE, PREPOSITIONAL, DATIVE -> input.substring(0, input.length()-1)+"и"; + case ACCUSATIVE -> input; + case INSTRUMENTAL -> input+"ю"; + default -> throw new IllegalArgumentException(); + }; } if(inputLower.endsWith("е") || inputLower.endsWith("ё") || inputLower.endsWith("и") || inputLower.endsWith("о") || inputLower.endsWith("у") || inputLower.endsWith("ы") || inputLower.endsWith("э") || inputLower.endsWith("ю")){ return input; @@ -639,278 +419,199 @@ public static String inflectFirstName(String input, User.Gender gender, Inflecto return input; } if(gender==User.Gender.FEMALE && (inputLower.endsWith("ь"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"и"; - case DATIVE: - return input.substring(0, input.length()-1)+"и"; - case ACCUSATIVE: - return input; - case INSTRUMENTAL: - return input+"ю"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"и"; - } + return switch(_case){ + case GENITIVE, PREPOSITIONAL, DATIVE -> input.substring(0, input.length()-1)+"и"; + case ACCUSATIVE -> input; + case INSTRUMENTAL -> input+"ю"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ь"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"я"; - case DATIVE: - return input.substring(0, input.length()-1)+"ю"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"я"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ем"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "я"; + case DATIVE -> "ю"; + case INSTRUMENTAL -> "ем"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(inputLower.endsWith("га") || inputLower.endsWith("ка") || inputLower.endsWith("ха") || inputLower.endsWith("ча") || inputLower.endsWith("ща") || inputLower.endsWith("жа")){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"и"; - case DATIVE: - return input.substring(0, input.length()-1)+"е"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"у"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ой"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE -> "и"; + case DATIVE, PREPOSITIONAL -> "е"; + case ACCUSATIVE -> "у"; + case INSTRUMENTAL -> "ой"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.FEMALE && (inputLower.endsWith("ша"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"и"; - case DATIVE: - return input.substring(0, input.length()-1)+"е"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"у"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ей"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE -> "и"; + case DATIVE, PREPOSITIONAL -> "е"; + case ACCUSATIVE -> "у"; + case INSTRUMENTAL -> "ей"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ша") || inputLower.endsWith("ча") || inputLower.endsWith("жа"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"и"; - case DATIVE: - return input.substring(0, input.length()-1)+"е"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"у"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ей"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE -> "и"; + case DATIVE, PREPOSITIONAL -> "е"; + case ACCUSATIVE -> "у"; + case INSTRUMENTAL -> "ей"; + default -> throw new IllegalArgumentException(); + }; } if(inputLower.endsWith("а")){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"ы"; - case DATIVE: - return input.substring(0, input.length()-1)+"е"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"у"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ой"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE -> "ы"; + case DATIVE, PREPOSITIONAL -> "е"; + case ACCUSATIVE -> "у"; + case INSTRUMENTAL -> "ой"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.FEMALE && (inputLower.endsWith("ка") || inputLower.endsWith("га") || inputLower.endsWith("ха"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"и"; - case DATIVE: - return input.substring(0, input.length()-1)+"е"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"у"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ой"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE -> "и"; + case DATIVE, PREPOSITIONAL -> "е"; + case ACCUSATIVE -> "у"; + case INSTRUMENTAL -> "ой"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.FEMALE && (inputLower.endsWith("ца"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"ы"; - case DATIVE: - return input.substring(0, input.length()-1)+"е"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"у"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ей"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE -> "ы"; + case DATIVE, PREPOSITIONAL -> "е"; + case ACCUSATIVE -> "у"; + case INSTRUMENTAL -> "ей"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.FEMALE && (inputLower.endsWith("а"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"ы"; - case DATIVE: - return input.substring(0, input.length()-1)+"е"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"у"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ой"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE -> "ы"; + case DATIVE, PREPOSITIONAL -> "е"; + case ACCUSATIVE -> "у"; + case INSTRUMENTAL -> "ой"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.FEMALE && (inputLower.endsWith("ия"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"и"; - case DATIVE: - return input.substring(0, input.length()-1)+"и"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"ю"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ей"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"и"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE, PREPOSITIONAL, DATIVE -> "и"; + case ACCUSATIVE -> "ю"; + case INSTRUMENTAL -> "ей"; + default -> throw new IllegalArgumentException(); + }; } if(inputLower.endsWith("я")){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"и"; - case DATIVE: - return input.substring(0, input.length()-1)+"е"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"ю"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ей"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE -> "и"; + case DATIVE, PREPOSITIONAL -> "е"; + case ACCUSATIVE -> "ю"; + case INSTRUMENTAL -> "ей"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ий"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"я"; - case DATIVE: - return input.substring(0, input.length()-1)+"ю"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"я"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ем"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"и"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "я"; + case DATIVE -> "ю"; + case INSTRUMENTAL -> "ем"; + case PREPOSITIONAL -> "и"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ей") || inputLower.endsWith("й"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"я"; - case DATIVE: - return input.substring(0, input.length()-1)+"ю"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"я"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ем"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "я"; + case DATIVE -> "ю"; + case INSTRUMENTAL -> "ем"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; + } + if(gender==User.Gender.MALE && (inputLower.endsWith("бек"))){ + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "ка"; + case DATIVE -> "ку"; + case INSTRUMENTAL -> "ком"; + case PREPOSITIONAL -> "ке"; + default -> throw new IllegalArgumentException(); + }; + } + if(gender==User.Gender.MALE && (inputLower.endsWith("ек") || inputLower.endsWith("ёк"))){ + return input.substring(0, input.length()-2)+switch(_case){ + case GENITIVE, ACCUSATIVE -> "ька"; + case DATIVE -> "ьку"; + case INSTRUMENTAL -> "ьком"; + case PREPOSITIONAL -> "ьке"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ш") || inputLower.endsWith("ж"))){ - switch(_case){ - case GENITIVE: - return input+"а"; - case DATIVE: - return input+"у"; - case ACCUSATIVE: - return input+"а"; - case INSTRUMENTAL: - return input+"ем"; - case PREPOSITIONAL: - return input+"е"; - } + return input+switch(_case){ + case GENITIVE, ACCUSATIVE -> "а"; + case DATIVE -> "у"; + case INSTRUMENTAL -> "ем"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("б") || inputLower.endsWith("в") || inputLower.endsWith("г") || inputLower.endsWith("д") || inputLower.endsWith("ж") || inputLower.endsWith("з") || inputLower.endsWith("к") || inputLower.endsWith("л") || inputLower.endsWith("м") || inputLower.endsWith("н") || inputLower.endsWith("п") || inputLower.endsWith("р") || inputLower.endsWith("с") || inputLower.endsWith("т") || inputLower.endsWith("ф") || inputLower.endsWith("х") || inputLower.endsWith("ц") || inputLower.endsWith("ч"))){ - switch(_case){ - case GENITIVE: - return input+"а"; - case DATIVE: - return input+"у"; - case ACCUSATIVE: - return input+"а"; - case INSTRUMENTAL: - return input+"ом"; - case PREPOSITIONAL: - return input+"е"; - } + return input+switch(_case){ + case GENITIVE, ACCUSATIVE -> "а"; + case DATIVE -> "у"; + case INSTRUMENTAL -> "ом"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(inputLower.endsWith("ния") || inputLower.endsWith("рия") || inputLower.endsWith("вия")){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"и"; - case DATIVE: - return input.substring(0, input.length()-1)+"и"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"ю"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ем"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"ем"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE, DATIVE -> "и"; + case ACCUSATIVE -> "ю"; + case INSTRUMENTAL, PREPOSITIONAL -> "ем"; + default -> throw new IllegalArgumentException(); + }; } return input; } public static String inflectMiddleName(String input, User.Gender gender, Inflector.Case _case, boolean firstWord){ + if(_case==Inflector.Case.NOMINATIVE) return input; String inputLower=input.toLowerCase(); if(firstWord && (inputLower.equals("борух"))){ return input; } if(gender==User.Gender.MALE && (inputLower.endsWith("мич") || inputLower.endsWith("ьич") || inputLower.endsWith("кич"))){ - switch(_case){ - case GENITIVE: - return input+"а"; - case DATIVE: - return input+"у"; - case ACCUSATIVE: - return input+"а"; - case INSTRUMENTAL: - return input+"ом"; - case PREPOSITIONAL: - return input+"е"; - } + return input+switch(_case){ + case GENITIVE, ACCUSATIVE -> "а"; + case DATIVE -> "у"; + case INSTRUMENTAL -> "ом"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.MALE && (inputLower.endsWith("ич"))){ - switch(_case){ - case GENITIVE: - return input+"а"; - case DATIVE: - return input+"у"; - case ACCUSATIVE: - return input+"а"; - case INSTRUMENTAL: - return input+"ем"; - case PREPOSITIONAL: - return input+"е"; - } + return input+switch(_case){ + case GENITIVE, ACCUSATIVE -> "а"; + case DATIVE -> "у"; + case INSTRUMENTAL -> "ем"; + case PREPOSITIONAL -> "е"; + default -> throw new IllegalArgumentException(); + }; } if(gender==User.Gender.FEMALE && (inputLower.endsWith("на"))){ - switch(_case){ - case GENITIVE: - return input.substring(0, input.length()-1)+"ы"; - case DATIVE: - return input.substring(0, input.length()-1)+"е"; - case ACCUSATIVE: - return input.substring(0, input.length()-1)+"у"; - case INSTRUMENTAL: - return input.substring(0, input.length()-1)+"ой"; - case PREPOSITIONAL: - return input.substring(0, input.length()-1)+"е"; - } + return input.substring(0, input.length()-1)+switch(_case){ + case GENITIVE -> "ы"; + case DATIVE, PREPOSITIONAL -> "е"; + case ACCUSATIVE -> "у"; + case INSTRUMENTAL -> "ой"; + default -> throw new IllegalArgumentException(); + }; } return input; } @@ -931,10 +632,10 @@ public static User.Gender genderForLastName(String input){ public static User.Gender genderForFirstName(String input){ input=input.toLowerCase(); - if(input.equals("сева") || input.equals("иона") || input.equals("муса") || input.equals("саша") || input.equals("алвард") || input.equals("валери") || input.equals("кири") || input.equals("анри") || input.equals("ким") || input.equals("райхон") || input.equals("закия") || input.equals("захария") || input.equals("женя")) + if(input.equals("сева") || input.equals("иона") || input.equals("муса") || input.equals("саша") || input.equals("алвард") || input.equals("валери") || input.equals("кири") || input.equals("анри") || input.equals("ким") || input.equals("райхон") || input.equals("закия") || input.equals("захария") || input.equals("женя") || input.equals("карен")) return User.Gender.UNKNOWN; - if(input.equals("абиба") || input.equals("савва") || input.equals("лёва") || input.equals("вова") || input.equals("ага") || input.equals("ахмедага") || input.equals("алиага") || input.equals("амирага") || input.equals("агга") || input.equals("серега") || input.equals("фейга") || input.equals("гога") || input.equals("алиада") || input.equals("муктада") || input.equals("абида") || input.equals("алда") || input.equals("маджуда") || input.equals("нурлыхуда") || input.equals("гиа") || input.equals("элиа") || input.equals("гарсиа") || input.equals("вавила") || input.equals("гавриила") || input.equals("генка") || input.equals("лука") || input.equals("дима") || input.equals("зосима") || input.equals("тима") || input.equals("фима") || input.equals("фома") || input.equals("кузьма") || input.equals("жора") || input.equals("миша") || input.equals("ермила") || input.equals("данила") || input.equals("гаврила") || input.equals("абдалла") || input.equals("аталла") || input.equals("абдилла") || input.equals("атилла") || input.equals("кайролла") || input.equals("абулла") || input.equals("абула") || input.equals("свитлана") || input.equals("бена") || input.equals("гена") || input.equals("агелина") || input.equals("джанна") || input.equals("кришна") || input.equals("степа") || input.equals("дра") || input.equals("назера") || input.equals("валера") || input.equals("эстера") || input.equals("двойра") || input.equals("калистра") || input.equals("заратустра") || input.equals("юра") || input.equals("иса") || input.equals("аиса") || input.equals("халиса") || input.equals("холиса") || input.equals("валенса") || input.equals("мусса") || input.equals("ата") || input.equals("паата") || input.equals("алета") || input.equals("никита") || input.equals("мота") || input.equals("шота") || input.equals("фаста") || input.equals("коста") || input.equals("маритта") || input.equals("малюта") || input.equals("васюта") || input.equals("вафа") || input.equals("мустафа") || input.equals("ганифа") || input.equals("лев") || input.equals("яков") || input.equals("шелли") || input.equals("константин") || input.equals("марсель") || input.equals("рамиль") || input.equals("эмиль") || input.equals("бактыгуль") || input.equals("даниэль") || input.equals("игорь") || input.equals("рауль") || input.equals("поль") || input.equals("анхель") || input.equals("михель") || input.equals("мигель") || input.equals("микель") || input.equals("микаиль") || input.equals("микаель") || input.equals("михаэль") || input.equals("самаэль") || input.equals("лазарь") || input.equals("алесь") || input.equals("олесь") || input.equals("шамиль") || input.equals("рафаэль") || input.equals("джамаль") || input.equals("арминэ") || input.equals("изя") || input.equals("кузя") || input.equals("гия") || input.equals("мазия") || input.equals("кирикия") || input.equals("ркия") || input.equals("еркия") || input.equals("эркия") || input.equals("гулия") || input.equals("аксания") || input.equals("закария") || input.equals("зекерия") || input.equals("гарсия") || input.equals("шендля") || input.equals("филя") || input.equals("вилля") || input.equals("толя") || input.equals("ваня") || input.equals("саня") || input.equals("загиря") || input.equals("боря") || input.equals("цайся") || input.equals("вася") || input.equals("ося") || input.equals("петя") || input.equals("витя") || input.equals("митя") || input.equals("костя") || input.equals("алья") || input.equals("илья") || input.equals("ларья")) + if(input.equals("абиба") || input.equals("савва") || input.equals("лёва") || input.equals("вова") || input.equals("ага") || input.equals("ахмедага") || input.equals("алиага") || input.equals("амирага") || input.equals("агга") || input.equals("серега") || input.equals("фейга") || input.equals("гога") || input.equals("алиада") || input.equals("муктада") || input.equals("абида") || input.equals("алда") || input.equals("маджуда") || input.equals("нурлыхуда") || input.equals("гиа") || input.equals("элиа") || input.equals("гарсиа") || input.equals("вавила") || input.equals("гавриила") || input.equals("генка") || input.equals("лука") || input.equals("дима") || input.equals("зосима") || input.equals("тима") || input.equals("фима") || input.equals("фома") || input.equals("кузьма") || input.equals("жора") || input.equals("миша") || input.equals("ермила") || input.equals("данила") || input.equals("гаврила") || input.equals("абдалла") || input.equals("аталла") || input.equals("абдилла") || input.equals("атилла") || input.equals("кайролла") || input.equals("абулла") || input.equals("абула") || input.equals("свитлана") || input.equals("бена") || input.equals("гена") || input.equals("агелина") || input.equals("джанна") || input.equals("кришна") || input.equals("степа") || input.equals("дра") || input.equals("назера") || input.equals("валера") || input.equals("эстера") || input.equals("двойра") || input.equals("калистра") || input.equals("заратустра") || input.equals("юра") || input.equals("иса") || input.equals("аиса") || input.equals("халиса") || input.equals("холиса") || input.equals("валенса") || input.equals("мусса") || input.equals("ата") || input.equals("паата") || input.equals("алета") || input.equals("никита") || input.equals("мота") || input.equals("шота") || input.equals("фаста") || input.equals("коста") || input.equals("маритта") || input.equals("малюта") || input.equals("васюта") || input.equals("вафа") || input.equals("мустафа") || input.equals("ганифа") || input.equals("лев") || input.equals("яков") || input.equals("шелли") || input.equals("константин") || input.equals("марсель") || input.equals("рамиль") || input.equals("эмиль") || input.equals("бактыгуль") || input.equals("даниэль") || input.equals("игорь") || input.equals("рауль") || input.equals("поль") || input.equals("анхель") || input.equals("михель") || input.equals("мигель") || input.equals("микель") || input.equals("микаиль") || input.equals("микаель") || input.equals("михаэль") || input.equals("самаэль") || input.equals("лазарь") || input.equals("алесь") || input.equals("олесь") || input.equals("шамиль") || input.equals("рафаэль") || input.equals("джамаль") || input.equals("арминэ") || input.equals("изя") || input.equals("кузя") || input.equals("гия") || input.equals("мазия") || input.equals("кирикия") || input.equals("ркия") || input.equals("еркия") || input.equals("эркия") || input.equals("гулия") || input.equals("аксания") || input.equals("закария") || input.equals("зекерия") || input.equals("гарсия") || input.equals("шендля") || input.equals("филя") || input.equals("вилля") || input.equals("толя") || input.equals("ваня") || input.equals("саня") || input.equals("загиря") || input.equals("боря") || input.equals("цайся") || input.equals("вася") || input.equals("ося") || input.equals("петя") || input.equals("витя") || input.equals("митя") || input.equals("костя") || input.equals("алья") || input.equals("илья") || input.equals("ларья") || input.equals("артём")) return User.Gender.MALE; if(input.equals("судаба") || input.equals("сураба") || input.equals("любава") || input.equals("джанлука") || input.equals("варвара") || input.equals("наташа") || input.equals("зайнаб") || input.equals("любов") || input.equals("сольвейг") || input.equals("шакед") || input.equals("аннаид") || input.equals("ингрид") || input.equals("синди") || input.equals("аллаберди") || input.equals("сандали") || input.equals("лали") || input.equals("натали") || input.equals("гулькай") || input.equals("алтынай") || input.equals("гюнай") || input.equals("гюльчитай") || input.equals("нурангиз") || input.equals("лиз") || input.equals("элиз") || input.equals("ботагоз") || input.equals("юлдуз") || input.equals("диляфруз") || input.equals("габи") || input.equals("сажи") || input.equals("фанни") || input.equals("мери") || input.equals("элдари") || input.equals("эльдари") || input.equals("хилари") || input.equals("хиллари") || input.equals("аннемари") || input.equals("розмари") || input.equals("товсари") || input.equals("ансари") || input.equals("одри") || input.equals("тери") || input.equals("ири") || input.equals("катри") || input.equals("мэри") || input.equals("сатаней") || input.equals("ефтений") || input.equals("верунчик") || input.equals("гюзел") || input.equals("этел") || input.equals("рэйчел") || input.equals("джил") || input.equals("мерил") || input.equals("нинелл") || input.equals("бурул") || input.equals("ахлам") || input.equals("майрам") || input.equals("махаррам") || input.equals("мириам") || input.equals("дилярам") || input.equals("асем") || input.equals("мерьем") || input.equals("мирьем") || input.equals("эркаим") || input.equals("гулаим") || input.equals("айгерим") || input.equals("марьям") || input.equals("мирьям") || input.equals("эван") || input.equals("гульжиган") || input.equals("айдан") || input.equals("айжан") || input.equals("вивиан") || input.equals("гульжиан") || input.equals("лилиан") || input.equals("мариан") || input.equals("саиман") || input.equals("джоан") || input.equals("чулпан") || input.equals("лоран") || input.equals("моран") || input.equals("джохан") || input.equals("гульшан") || input.equals("аделин") || input.equals("жаклин") || input.equals("карин") || input.equals("каролин") || input.equals("каталин") || input.equals("катрин") || input.equals("керстин") || input.equals("кэтрин") || input.equals("мэрилин") || input.equals("рузалин") || input.equals("хелин") || input.equals("цеткин") || input.equals("ширин") || input.equals("элисон") || input.equals("дурсун") || input.equals("кристин") || input.equals("гульжиян") || input.equals("марьян") || input.equals("ренато") || input.equals("зейнеп") || input.equals("санабар") || input.equals("дильбар") || input.equals("гулизар") || input.equals("гульзар") || input.equals("пилар") || input.equals("дагмар") || input.equals("элинар") || input.equals("нилуфар") || input.equals("анхар") || input.equals("гаухар") || input.equals("естер") || input.equals("эстер") || input.equals("дженнифер") || input.equals("линор") || input.equals("элинор") || input.equals("элеонор") || input.equals("айнур") || input.equals("гульнур") || input.equals("шамсинур") || input.equals("элнур") || input.equals("ильсияр") || input.equals("нигяр") || input.equals("сигитас") || input.equals("агнес") || input.equals("анес") || input.equals("долорес") || input.equals("инес") || input.equals("анаис") || input.equals("таис") || input.equals("эллис") || input.equals("элис") || input.equals("кларис") || input.equals("амнерис") || input.equals("айрис") || input.equals("дорис") || input.equals("беатрис") || input.equals("грейс") || input.equals("грэйс") || input.equals("ботагос") || input.equals("маргос") || input.equals("джулианс") || input.equals("арус") || input.equals("диляфрус") || input.equals("саодат") || input.equals("зулхижат") || input.equals("хамат") || input.equals("патимат") || input.equals("хатимат") || input.equals("альжанат") || input.equals("маймунат") || input.equals("гульшат") || input.equals("биргит") || input.equals("рут") || input.equals("иргаш") || input.equals("айнаш") || input.equals("агнеш") || input.equals("зауреш") || input.equals("тэрбиш") || input.equals("ануш") || input.equals("азгануш") || input.equals("гаруш") || input.equals("николь") || input.equals("адась") || input.equals("любовь") || input.equals("руфь") || input.equals("ассоль") || input.equals("юдифь") || input.equals("гретель") || input.equals("греттель") || input.equals("адель") || input.equals("жизель") || input.equals("гузель") || input.equals("нинель") || input.equals("этель") || input.equals("асель") || input.equals("агарь") || input.equals("рахиль") || input.equals("фамарь") || input.equals("иаиль") || input.equals("есфирь") || input.equals("астинь") || input.equals("рапунцель") || input.equals("афиля") || input.equals("тафиля") || input.equals("фаня") || input.equals("аня")) @@ -943,7 +644,7 @@ public static User.Gender genderForFirstName(String input){ if(input.endsWith("улла")) return User.Gender.UNKNOWN; - if(input.endsWith("аба") || input.endsWith("б") || input.endsWith("ав") || input.endsWith("ев") || input.endsWith("ов") || input.endsWith("г") || input.endsWith("д") || input.endsWith("ж") || input.endsWith("з") || input.endsWith("би") || input.endsWith("ди") || input.endsWith("жи") || input.endsWith("али") || input.endsWith("ри") || input.endsWith("ай") || input.endsWith("ей") || input.endsWith("ий") || input.endsWith("ой") || input.endsWith("ый") || input.endsWith("к") || input.endsWith("л") || input.endsWith("ам") || input.endsWith("ем") || input.endsWith("им") || input.endsWith("ом") || input.endsWith("ум") || input.endsWith("ым") || input.endsWith("ям") || input.endsWith("ан") || input.endsWith("бен") || input.endsWith("вен") || input.endsWith("ген") || input.endsWith("ден") || input.endsWith("ин") || input.endsWith("сейн") || input.endsWith("он") || input.endsWith("ун") || input.endsWith("ян") || input.endsWith("ио") || input.endsWith("ло") || input.endsWith("ро") || input.endsWith("то") || input.endsWith("шо") || input.endsWith("п") || input.endsWith("ар") || input.endsWith("др") || input.endsWith("ер") || input.endsWith("ир") || input.endsWith("ор") || input.endsWith("тр") || input.endsWith("ур") || input.endsWith("ыр") || input.endsWith("яр") || input.endsWith("ас") || input.endsWith("ес") || input.endsWith("ис") || input.endsWith("йс") || input.endsWith("кс") || input.endsWith("мс") || input.endsWith("ос") || input.endsWith("нс") || input.endsWith("рс") || input.endsWith("ус") || input.endsWith("юс") || input.endsWith("яс") || input.endsWith("ат") || input.endsWith("мет") || input.endsWith("кт") || input.endsWith("нт") || input.endsWith("рт") || input.endsWith("ст") || input.endsWith("ут") || input.endsWith("ф") || input.endsWith("х") || input.endsWith("ш") || input.endsWith("ы") || input.endsWith("сь") || input.endsWith("емеля") || input.endsWith("коля")) + if(input.endsWith("аба") || input.endsWith("б") || input.endsWith("ав") || input.endsWith("ев") || input.endsWith("ов") || input.endsWith("г") || input.endsWith("д") || input.endsWith("ж") || input.endsWith("з") || input.endsWith("би") || input.endsWith("ди") || input.endsWith("жи") || input.endsWith("али") || input.endsWith("ри") || input.endsWith("ай") || input.endsWith("ей") || input.endsWith("ий") || input.endsWith("ой") || input.endsWith("ый") || input.endsWith("к") || input.endsWith("л") || input.endsWith("ам") || input.endsWith("ем") || input.endsWith("им") || input.endsWith("ом") || input.endsWith("ум") || input.endsWith("ым") || input.endsWith("ям") || input.endsWith("ан") || input.endsWith("ен") || input.endsWith("ин") || input.endsWith("сейн") || input.endsWith("он") || input.endsWith("ун") || input.endsWith("ян") || input.endsWith("ио") || input.endsWith("ло") || input.endsWith("ро") || input.endsWith("то") || input.endsWith("шо") || input.endsWith("п") || input.endsWith("ар") || input.endsWith("др") || input.endsWith("ер") || input.endsWith("ир") || input.endsWith("ор") || input.endsWith("тр") || input.endsWith("ур") || input.endsWith("ыр") || input.endsWith("яр") || input.endsWith("ас") || input.endsWith("ес") || input.endsWith("ис") || input.endsWith("йс") || input.endsWith("кс") || input.endsWith("мс") || input.endsWith("ос") || input.endsWith("нс") || input.endsWith("рс") || input.endsWith("ус") || input.endsWith("юс") || input.endsWith("яс") || input.endsWith("ат") || input.endsWith("мет") || input.endsWith("кт") || input.endsWith("нт") || input.endsWith("рт") || input.endsWith("ст") || input.endsWith("ут") || input.endsWith("ф") || input.endsWith("х") || input.endsWith("ш") || input.endsWith("ы") || input.endsWith("сь") || input.endsWith("емеля") || input.endsWith("коля")) return User.Gender.MALE; if(input.endsWith("иба") || input.endsWith("люба") || input.endsWith("лава") || input.endsWith("ева") || input.endsWith("га") || input.endsWith("да") || input.endsWith("еа") || input.endsWith("иза") || input.endsWith("иа") || input.endsWith("ика") || input.endsWith("нка") || input.endsWith("ска") || input.endsWith("ела") || input.endsWith("ила") || input.endsWith("лла") || input.endsWith("эла") || input.endsWith("има") || input.endsWith("на") || input.endsWith("ра") || input.endsWith("са") || input.endsWith("та") || input.endsWith("фа") || input.endsWith("елли") || input.endsWith("еса") || input.endsWith("сса") || input.endsWith("гуль") || input.endsWith("нуэль") || input.endsWith("гюль") || input.endsWith("нэ") || input.endsWith("ая") || input.endsWith("ея") || input.endsWith("ия") || input.endsWith("йя") || input.endsWith("ля") || input.endsWith("мя") || input.endsWith("оя") || input.endsWith("ря") || input.endsWith("ся") || input.endsWith("вья") || input.endsWith("лья") || input.endsWith("мья") || input.endsWith("нья") || input.endsWith("рья") || input.endsWith("сья") || input.endsWith("тья") || input.endsWith("фья") || input.endsWith("зя")) From e0871aa8c69407e8bb95f76376fad4333f5b7a47 Mon Sep 17 00:00:00 2001 From: Colin Reeder Date: Mon, 27 Dec 2021 14:41:39 -0700 Subject: [PATCH 18/65] Allow tokens in signature value --- src/main/java/smithereen/Utils.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/smithereen/Utils.java b/src/main/java/smithereen/Utils.java index 44801ad0..9a6272ba 100644 --- a/src/main/java/smithereen/Utils.java +++ b/src/main/java/smithereen/Utils.java @@ -93,7 +93,7 @@ public class Utils{ public static final Pattern URL_PATTERN=Pattern.compile("\\b(https?:\\/\\/)?("+IDN_DOMAIN_REGEX+")(?:\\:\\d+)?((?:\\/(?:[\\w\\.@%:!+-]|\\([^\\s]+?\\))+)*)(\\?(?:\\w+(?:=(?:[\\w\\.@%:!+-]|\\([^\\s]+?\\))+&?)?)+)?(#(?:[\\w\\.@%:!+-]|\\([^\\s]+?\\))+)?", Pattern.CASE_INSENSITIVE); public static final Pattern MENTION_PATTERN=Pattern.compile("@([a-zA-Z0-9._-]+)(?:@("+IDN_DOMAIN_REGEX+"))?"); public static final Pattern USERNAME_DOMAIN_PATTERN=Pattern.compile("@?([a-zA-Z0-9._-]+)@("+IDN_DOMAIN_REGEX+")"); - private static final Pattern SIGNATURE_HEADER_PATTERN=Pattern.compile("([a-zA-Z0-9]+)=\\\"((?:[^\\\"\\\\]|\\\\.)*)\\\"\\s*([,;])?\\s*"); + private static final Pattern SIGNATURE_HEADER_PATTERN=Pattern.compile("([!#$%^'*+\\-.^_`|~0-9A-Za-z]+)=(?:(?:\\\"((?:[^\\\"\\\\]|\\\\.)*)\\\")|([!#$%^'*+\\-.^_`|~0-9A-Za-z]+))\\s*([,;])?\\s*"); private static final Pattern NON_ASCII_PATTERN=Pattern.compile("\\P{ASCII}"); public static final Gson gson=new GsonBuilder() @@ -683,8 +683,14 @@ public static List> parseSignatureHeader(String header){ HashMap curMap=new HashMap<>(); while(matcher.find()){ String key=matcher.group(1); - String value=matcher.group(2).replace("\\\"", "\"").replace("\\\\", "\\"); - String separator=matcher.group(3); + + String value=matcher.group(2); + if(value == null) { + value = matcher.group(3); + } + value = value.replace("\\\"", "\"").replace("\\\\", "\\"); + + String separator=matcher.group(4); curMap.put(key, value); if(separator==null || ";".equals(separator)){ res.add(curMap); From b290b78cde8f15fa78e948f123c75f35de8ee98c Mon Sep 17 00:00:00 2001 From: Grishka Date: Wed, 29 Dec 2021 02:37:21 +0300 Subject: [PATCH 19/65] Use plaintext http for webfinger if useHTTP is set --- src/main/java/smithereen/activitypub/ActivityPub.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/smithereen/activitypub/ActivityPub.java b/src/main/java/smithereen/activitypub/ActivityPub.java index 1c466284..1ea62eb4 100644 --- a/src/main/java/smithereen/activitypub/ActivityPub.java +++ b/src/main/java/smithereen/activitypub/ActivityPub.java @@ -205,7 +205,7 @@ private static URI doWebfingerRequest(String username, String domain, String uri String resource="acct:"+username+"@"+domain; String url; if(StringUtils.isEmpty(uriTemplate)){ - url="https://"+domain+"/.well-known/webfinger?resource="+resource; + url=(Config.useHTTP ? "http" : "https")+"://"+domain+"/.well-known/webfinger?resource="+resource; }else{ url=uriTemplate.replace("{uri}", resource); } @@ -260,7 +260,7 @@ public static URI resolveUsername(String username, String domain) throws IOExcep }catch(ObjectNotFoundException x){ if(redirect==null){ Request req=new Request.Builder() - .url("https://"+domain+"/.well-known/host-meta") + .url((Config.useHTTP ? "http" : "https")+"://"+domain+"/.well-known/host-meta") .header("Accept", "application/xrd+xml") .build(); Response resp=httpClient.newCall(req).execute(); @@ -284,7 +284,7 @@ public static URI resolveUsername(String username, String domain) throws IOExcep if("lrdd".equals(rel) && "application/xrd+xml".equals(type)){ if((template.startsWith("https://") || (Config.useHTTP && template.startsWith("http://"))) && template.contains("{uri}")){ synchronized(ActivityPub.class){ - if(("https://"+domain+"/.well-known/webfinger?resource={uri}").equals(template)){ + if(template.endsWith("://"+domain+"/.well-known/webfinger?resource={uri}")){ // this isn't a real redirect domainRedirects.put(domain, ""); // don't repeat the request, we already know that username doesn't exist (but the webfinger endpoint does) From 2563db7c8f58bafc1937e75187a118d9571bc3a9 Mon Sep 17 00:00:00 2001 From: Grishka Date: Sun, 2 Jan 2022 22:09:18 +0300 Subject: [PATCH 20/65] Event reminders --- src/main/java/smithereen/Utils.java | 2 + .../controllers/GroupsController.java | 106 +++++++++++++++++- .../java/smithereen/data/EventReminder.java | 26 +++++ .../smithereen/storage/DatabaseUtils.java | 2 + .../java/smithereen/storage/GroupStorage.java | 16 +++ .../java/smithereen/storage/PostStorage.java | 6 +- .../java/smithereen/storage/UserStorage.java | 2 + .../java/smithereen/templates/Templates.java | 9 +- src/main/resources/langs/en.json | 17 ++- src/main/resources/langs/ru.json | 17 ++- .../resources/templates/common/left_menu.twig | 25 ++++- 11 files changed, 213 insertions(+), 15 deletions(-) create mode 100644 src/main/java/smithereen/data/EventReminder.java diff --git a/src/main/java/smithereen/Utils.java b/src/main/java/smithereen/Utils.java index 9a6272ba..3c2fc369 100644 --- a/src/main/java/smithereen/Utils.java +++ b/src/main/java/smithereen/Utils.java @@ -566,6 +566,8 @@ public static String preprocessPostHTML(String text, MentionCallback mentionCall } public static String postprocessPostHTMLForDisplay(String text){ + if(text==null) + return ""; Document doc=Jsoup.parseBodyFragment(text); for(Element el:doc.getElementsByTag("a")){ diff --git a/src/main/java/smithereen/controllers/GroupsController.java b/src/main/java/smithereen/controllers/GroupsController.java index ca4e3ca6..ae847025 100644 --- a/src/main/java/smithereen/controllers/GroupsController.java +++ b/src/main/java/smithereen/controllers/GroupsController.java @@ -7,12 +7,24 @@ import java.sql.SQLException; import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalUnit; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import smithereen.ApplicationContext; +import smithereen.LruCache; import smithereen.Utils; import smithereen.activitypub.ActivityPubWorker; +import smithereen.data.EventReminder; import smithereen.data.ForeignGroup; import smithereen.data.Group; import smithereen.data.GroupAdmin; @@ -26,6 +38,7 @@ import smithereen.exceptions.UserErrorException; import smithereen.storage.GroupStorage; import smithereen.storage.NewsfeedStorage; +import smithereen.util.BackgroundTaskRunner; import spark.utils.StringUtils; import static smithereen.Utils.wrapError; @@ -34,6 +47,7 @@ public class GroupsController{ private static final Logger LOG=LoggerFactory.getLogger(GroupsController.class); private final ApplicationContext context; + private final LruCache eventRemindersCache=new LruCache<>(500); public GroupsController(ApplicationContext context){ this.context=context; @@ -108,6 +122,22 @@ public Group getLocalGroupOrThrow(int id){ return group; } + public List getGroupsByIdAsList(Collection ids){ + try{ + return GroupStorage.getByIdAsList(ids); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public Map getGroupsByIdAsMap(Collection ids){ + try{ + return GroupStorage.getById(ids); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + public PaginatedList getMembers(@NotNull Group group, int offset, int count, boolean tentative){ try{ return GroupStorage.getMembers(group.id, offset, count, tentative); @@ -166,9 +196,20 @@ public void enforceUserAdminLevel(@NotNull Group group, @NotNull User user, @Not public void updateGroupInfo(@NotNull Group group, @NotNull User admin, String name, String aboutSrc, Instant eventStart, Instant eventEnd){ try{ enforceUserAdminLevel(group, admin, Group.AdminLevel.ADMIN); - String about=Utils.preprocessPostHTML(aboutSrc, null); + String about=StringUtils.isNotEmpty(aboutSrc) ? Utils.preprocessPostHTML(aboutSrc, null) : null; GroupStorage.updateGroupGeneralInfo(group, name, aboutSrc, about, eventStart, eventEnd); ActivityPubWorker.getInstance().sendUpdateGroupActivity(group); + if(group.isEvent()){ + BackgroundTaskRunner.getInstance().submit(()->{ + try{ + synchronized(eventRemindersCache){ + GroupStorage.getAllMembersAsStream(group.id).boxed().forEach(eventRemindersCache::remove); + } + }catch(SQLException x){ + LOG.warn("error getting group members", x); + } + }); + } }catch(SQLException x){ throw new InternalServerErrorException(x); } @@ -206,6 +247,11 @@ public void joinGroup(@NotNull Group group, @NotNull User user, boolean tentativ ActivityPubWorker.getInstance().sendAddToGroupsCollectionActivity(user, group); } NewsfeedStorage.putEntry(user.id, group.id, group.isEvent() ? NewsfeedEntry.Type.JOIN_EVENT : NewsfeedEntry.Type.JOIN_GROUP, null); + if(group.isEvent()){ + synchronized(eventRemindersCache){ + eventRemindersCache.remove(user.id); + } + } }catch(SQLException x){ throw new InternalServerErrorException(x); } @@ -223,6 +269,64 @@ public void leaveGroup(@NotNull Group group, @NotNull User user){ } ActivityPubWorker.getInstance().sendRemoveFromGroupsCollectionActivity(user, group); NewsfeedStorage.deleteEntry(user.id, group.id, group.isEvent() ? NewsfeedEntry.Type.JOIN_EVENT : NewsfeedEntry.Type.JOIN_GROUP); + if(group.isEvent()){ + synchronized(eventRemindersCache){ + eventRemindersCache.remove(user.id); + } + } + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public EventReminder getUserEventReminder(@NotNull User user, @NotNull ZoneId timeZone){ + synchronized(eventRemindersCache){ + EventReminder reminder=eventRemindersCache.get(user.id); + if(reminder!=null){ + if(System.currentTimeMillis()-reminder.createdAt.toEpochMilli()<3600_000L && LocalDate.ofInstant(reminder.createdAt, timeZone).equals(LocalDate.now(timeZone))) + return reminder; + else + eventRemindersCache.remove(user.id); + } + } + try{ + List events=GroupStorage.getUpcomingEvents(user.id); + EventReminder reminder=new EventReminder(); + reminder.createdAt=Instant.now(); + if(events.isEmpty()){ + reminder.groupIDs=Collections.emptyList(); + synchronized(eventRemindersCache){ + eventRemindersCache.put(user.id, reminder); + return reminder; + } + } + + ZonedDateTime now=ZonedDateTime.now(timeZone); + Instant todayEnd=now.toInstant().plusNanos(now.until(ZonedDateTime.of(now.getYear(), now.getMonthValue(), now.getDayOfMonth()+1, 0, 0, 0, 0, timeZone), ChronoUnit.NANOS)); + Instant tomorrowEnd=todayEnd.plus(1, ChronoUnit.DAYS); + List eventsToday=new ArrayList<>(), eventsTomorrow=new ArrayList<>(); + for(Group g:events){ + if(g.eventStartTime.isBefore(todayEnd)){ + eventsToday.add(g.id); + }else if(g.eventStartTime.isBefore(tomorrowEnd)){ + eventsTomorrow.add(g.id); + } + } + + if(!eventsToday.isEmpty()){ + reminder.groupIDs=eventsToday; + reminder.day=LocalDate.now(timeZone); + }else if(!eventsTomorrow.isEmpty()){ + reminder.groupIDs=eventsTomorrow; + reminder.day=LocalDate.now(timeZone).plusDays(1); + }else{ + reminder.groupIDs=Collections.emptyList(); + } + + synchronized(eventRemindersCache){ + eventRemindersCache.put(user.id, reminder); + return reminder; + } }catch(SQLException x){ throw new InternalServerErrorException(x); } diff --git a/src/main/java/smithereen/data/EventReminder.java b/src/main/java/smithereen/data/EventReminder.java new file mode 100644 index 00000000..aeb17ff1 --- /dev/null +++ b/src/main/java/smithereen/data/EventReminder.java @@ -0,0 +1,26 @@ +package smithereen.data; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.List; + +public class EventReminder{ + /** + * Day on which these events are. + */ + public LocalDate day; + /** + * When this reminder was created. + */ + public Instant createdAt; + public List groupIDs; + + @Override + public String toString(){ + return "EventReminder{"+ + "day="+day+ + ", createdAt="+createdAt+ + ", groupIDs="+groupIDs+ + '}'; + } +} diff --git a/src/main/java/smithereen/storage/DatabaseUtils.java b/src/main/java/smithereen/storage/DatabaseUtils.java index 11ad155f..e8f24f60 100644 --- a/src/main/java/smithereen/storage/DatabaseUtils.java +++ b/src/main/java/smithereen/storage/DatabaseUtils.java @@ -87,6 +87,7 @@ public boolean tryAdvance(IntConsumer action){ action.accept(res.getInt(1)); return true; } + res.close(); }catch(SQLException x){ throw new UncheckedSQLException(x); } @@ -99,6 +100,7 @@ public void forEachRemaining(IntConsumer action){ while(res.next()){ action.accept(res.getInt(1)); } + res.close(); }catch(SQLException x){ throw new UncheckedSQLException(x); } diff --git a/src/main/java/smithereen/storage/GroupStorage.java b/src/main/java/smithereen/storage/GroupStorage.java index b2deda08..75195605 100644 --- a/src/main/java/smithereen/storage/GroupStorage.java +++ b/src/main/java/smithereen/storage/GroupStorage.java @@ -25,6 +25,7 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.IntStream; import smithereen.Config; import smithereen.LruCache; @@ -881,4 +882,19 @@ static String getQSearchStringForGroup(Group group){ s+=" "+group.domain; return s; } + + public static List getUpcomingEvents(int userID) throws SQLException{ + Connection conn=DatabaseConnectionManager.getConnection(); + PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT group_id, event_start_time FROM group_memberships JOIN `groups` ON `groups`.id=group_memberships.group_id WHERE user_id=? AND accepted=1 AND `groups`.type=1 AND event_start_time>NOW() AND event_start_time0){ conn.createStatement().execute("UPDATE wall_posts SET reply_count=GREATEST(1, reply_count)-1 WHERE id IN ("+Arrays.stream(post.replyKey).mapToObj(String::valueOf).collect(Collectors.joining(","))+")"); diff --git a/src/main/java/smithereen/storage/UserStorage.java b/src/main/java/smithereen/storage/UserStorage.java index 42f06097..30c3f8c6 100644 --- a/src/main/java/smithereen/storage/UserStorage.java +++ b/src/main/java/smithereen/storage/UserStorage.java @@ -34,6 +34,7 @@ import smithereen.activitypub.ContextCollector; import smithereen.data.Account; import smithereen.data.BirthdayReminder; +import smithereen.data.EventReminder; import smithereen.data.ForeignUser; import smithereen.data.FriendRequest; import smithereen.data.FriendshipStatus; @@ -52,6 +53,7 @@ public class UserStorage{ private static LruCache cacheByActivityPubID=new LruCache<>(500); private static LruCache accountCache=new LruCache<>(500); private static final LruCache birthdayReminderCache=new LruCache<>(500); + private static final LruCache eventReminderCache=new LruCache<>(500); public static synchronized User getById(int id) throws SQLException{ User user=cache.get(id); diff --git a/src/main/java/smithereen/templates/Templates.java b/src/main/java/smithereen/templates/Templates.java index 0e17e5f3..36191caa 100644 --- a/src/main/java/smithereen/templates/Templates.java +++ b/src/main/java/smithereen/templates/Templates.java @@ -22,8 +22,10 @@ import smithereen.Utils; import smithereen.data.Account; import smithereen.data.BirthdayReminder; +import smithereen.data.EventReminder; import smithereen.data.SessionInfo; import smithereen.data.UserNotifications; +import smithereen.exceptions.InternalServerErrorException; import smithereen.lang.Lang; import smithereen.storage.NotificationsStorage; import smithereen.storage.UserStorage; @@ -76,8 +78,13 @@ public static void addGlobalParamsToTemplate(Request req, RenderedTemplateRespon model.with("birthdayUsers", UserStorage.getByIdAsList(reminder.userIDs)); model.with("birthdaysAreToday", reminder.day.equals(today)); } + EventReminder eventReminder=Utils.context(req).getGroupsController().getUserEventReminder(account.user, tz.toZoneId()); + if(!eventReminder.groupIDs.isEmpty()){ + model.with("eventReminderEvents", Utils.context(req).getGroupsController().getGroupsByIdAsList(eventReminder.groupIDs)); + model.with("eventsAreToday", eventReminder.day.equals(today)); + } }catch(SQLException x){ - throw new RuntimeException(x); + throw new InternalServerErrorException(x); } } } diff --git a/src/main/resources/langs/en.json b/src/main/resources/langs/en.json index e6fbe8db..a1e51999 100644 --- a/src/main/resources/langs/en.json +++ b/src/main/resources/langs/en.json @@ -399,8 +399,8 @@ "notifications_empty": "Your notifications will appear here.", "wall_empty": "No one has written anything here... Yet.", "reminder": "Reminder", - "reminder_today": "Today", - "reminder_tomorrow": "Tomorrow", + "birthday_reminder_today": "Today", + "birthday_reminder_tomorrow": "Tomorrow", "birthday_reminder_before": "", "birthday_reminder_middle": " is ", "birthday_reminder_after": "'s birthday.", @@ -484,5 +484,16 @@ "tentative_members": "Not sure", "X_tentative_members": "{count} not sure", "summary_event_X_members": "{count, plural, one {One person is} other {# people are}} attending", - "summary_event_X_tentative_members": "{count, plural, one {One person is} other {# people are}} not sure" + "summary_event_X_tentative_members": "{count, plural, one {One person is} other {# people are}} not sure", + "event_reminder_before": "", + "event_reminder_middle": "{count, select, 1 {Event} other {Events}} ", + "event_reminder_after": ".", + "event_reminder_separator": ", ", + "event_reminder_separator_last": " and ", + "event_reminder_before_day": " takes place ", + "birthday_reminder_before_day": "", + "event_reminder_day_pos": "end", + "birthday_reminder_day_pos": "start", + "event_reminder_today": "today", + "event_reminder_tomorrow": "tomorrow" } \ No newline at end of file diff --git a/src/main/resources/langs/ru.json b/src/main/resources/langs/ru.json index 385efe00..c51eb1e6 100644 --- a/src/main/resources/langs/ru.json +++ b/src/main/resources/langs/ru.json @@ -401,8 +401,8 @@ "notifications_empty": "Ваши уведомления появятся здесь.", "wall_empty": "Здесь никто ничего не написал... Пока.", "reminder": "Напоминание", - "reminder_today": "Сегодня", - "reminder_tomorrow": "Завтра", + "birthday_reminder_today": "Сегодня", + "birthday_reminder_tomorrow": "Завтра", "birthday_reminder_before": "", "birthday_reminder_middle": " день рождения ", "birthday_reminder_after": ".", @@ -486,5 +486,16 @@ "tentative_members": "Возможные участники", "X_tentative_members": "{count, plural, one {# не уверен} other {# не уверены}}", "summary_event_X_members": "{count, plural, one {# человек пойдёт} few {# человека пойдут} other {# человек пойдут}} на встречу", - "summary_event_X_tentative_members": "{count, plural, one {# человек не уверен, что придёт} few {# человека не уверены, что придут} other {# человек не уверены, что придут}}" + "summary_event_X_tentative_members": "{count, plural, one {# человек не уверен, что придёт} few {# человека не уверены, что придут} other {# человек не уверены, что придут}}", + "event_reminder_before": "", + "event_reminder_middle": " {count, select, 1 {состоится встреча} other {состоятся встречи}} ", + "event_reminder_after": ".", + "event_reminder_separator": ", ", + "event_reminder_separator_last": " и ", + "event_reminder_before_day": "", + "birthday_reminder_before_day": "", + "event_reminder_day_pos": "start", + "birthday_reminder_day_pos": "start", + "event_reminder_today": "Сегодня", + "event_reminder_tomorrow": "Завтра" } \ No newline at end of file diff --git a/src/main/resources/templates/common/left_menu.twig b/src/main/resources/templates/common/left_menu.twig index cd2b264f..b68914e2 100644 --- a/src/main/resources/templates/common/left_menu.twig +++ b/src/main/resources/templates/common/left_menu.twig @@ -19,15 +19,32 @@ {% if birthdayUsers is not empty %}

        {{ L('reminder') }}

        - {{ L('birthday_reminder_before') -}} - {{- L(birthdaysAreToday ? 'reminder_today' : 'reminder_tomorrow') -}} - {{- L('birthday_reminder_middle') -}} + {{ L('birthday_reminder_before', {'count': birthdayUsers | length}) -}} + {%- if L('birthday_reminder_day_pos').toString()!="end" %}{{ L(birthdaysAreToday ? 'birthday_reminder_today' : 'birthday_reminder_tomorrow') }}{% endif -%} + {{- L('birthday_reminder_middle', {'count': birthdayUsers | length}) -}} {%- for user in birthdayUsers -%} {{ L('birthday_reminder_name', {'name': user.firstLastAndGender}) }} {%- if not loop.last -%} {{- L(loop.revindex>1 ? 'birthday_reminder_separator' : 'birthday_reminder_separator_last') -}} {%- endif -%} {%- endfor -%} - {{- L('birthday_reminder_after') -}} + {%- if L('birthday_reminder_day_pos').toString()=="end" %}{{ L('birthday_reminder_before_day', {'count': birthdayUsers | length}) }}{{ L(birthdaysAreToday ? 'birthday_reminder_today' : 'birthday_reminder_tomorrow') }}{% endif -%} + {{- L('birthday_reminder_after', {'count': birthdayUsers | length}) -}}
        {% endif %} +{% if eventReminderEvents is not empty %} +
        +

        {{ L('reminder') }}

        + {{ L('event_reminder_before', {'count': eventReminderEvents | length}) -}} + {%- if L('event_reminder_day_pos').toString()!="end" %}{{ L(eventsAreToday ? 'event_reminder_today' : 'event_reminder_tomorrow') }}{% endif -%} + {{- L('event_reminder_middle', {'count': eventReminderEvents | length}) -}} + {%- for event in eventReminderEvents -%} + {{ event.name }} + {%- if not loop.last -%} + {{- L(loop.revindex>1 ? 'event_reminder_separator' : 'event_reminder_separator_last') -}} + {%- endif -%} + {%- endfor -%} + {%- if L('event_reminder_day_pos').toString()=="end" %}{{ L('event_reminder_before_day', {'count': eventReminderEvents | length}) }}{{ L(eventsAreToday ? 'event_reminder_today' : 'event_reminder_tomorrow') }}{% endif -%} + {{- L('event_reminder_after', {'count': eventReminderEvents | length}) -}} +
        +{% endif %} \ No newline at end of file From c74a826e71cb848e54d8b7acaa91341773331e0b Mon Sep 17 00:00:00 2001 From: Grishka Date: Thu, 6 Jan 2022 04:00:09 +0300 Subject: [PATCH 21/65] Event calendar --- .../smithereen/SmithereenApplication.java | 2 + src/main/java/smithereen/Utils.java | 4 + .../controllers/GroupsController.java | 24 +- .../controllers/UsersController.java | 35 +++ .../smithereen/data/ActorWithDescription.java | 6 + .../java/smithereen/data/ForeignUser.java | 3 +- src/main/java/smithereen/data/User.java | 6 +- src/main/java/smithereen/lang/Lang.java | 12 +- .../java/smithereen/routes/GroupsRoutes.java | 212 +++++++++++++++++- .../java/smithereen/routes/ProfileRoutes.java | 2 +- .../smithereen/routes/SettingsRoutes.java | 16 +- .../smithereen/storage/DatabaseUtils.java | 5 + .../java/smithereen/storage/GroupStorage.java | 4 +- .../smithereen/storage/SQLQueryBuilder.java | 8 +- .../java/smithereen/storage/UserStorage.java | 44 +++- .../templates/PictureForAvatarFilter.java | 11 +- src/main/resources/langs/de.json | 39 +--- src/main/resources/langs/en.json | 57 ++--- src/main/resources/langs/es.json | 39 +--- src/main/resources/langs/pl.json | 39 +--- src/main/resources/langs/ru.json | 57 ++--- .../templates/common/actor_list.twig | 18 ++ .../templates/common/events_tabbar.twig | 1 + .../templates/common/group_admins.twig | 18 -- .../resources/templates/common/left_menu.twig | 8 +- .../desktop/events_actual_calendar.twig | 35 +++ .../templates/desktop/events_calendar.twig | 57 +++++ .../templates/desktop/friend_requests.twig | 63 +++--- .../resources/templates/desktop/friends.twig | 16 +- .../resources/templates/desktop/groups.twig | 17 +- .../templates/mobile/events_calendar.twig | 38 ++++ src/main/web/common.scss | 3 + src/main/web/common_ts/Helpers.ts | 4 + src/main/web/desktop.scss | 155 ++++++++++++- src/main/web/img/arrows_lr.svg | 3 + src/main/web/mobile.scss | 3 + 36 files changed, 759 insertions(+), 305 deletions(-) create mode 100644 src/main/java/smithereen/data/ActorWithDescription.java create mode 100644 src/main/resources/templates/common/actor_list.twig delete mode 100644 src/main/resources/templates/common/group_admins.twig create mode 100644 src/main/resources/templates/desktop/events_actual_calendar.twig create mode 100644 src/main/resources/templates/desktop/events_calendar.twig create mode 100644 src/main/resources/templates/mobile/events_calendar.twig create mode 100644 src/main/web/img/arrows_lr.svg diff --git a/src/main/java/smithereen/SmithereenApplication.java b/src/main/java/smithereen/SmithereenApplication.java index 30524e27..74e3cd21 100644 --- a/src/main/java/smithereen/SmithereenApplication.java +++ b/src/main/java/smithereen/SmithereenApplication.java @@ -399,6 +399,8 @@ public static void main(String[] args){ getLoggedIn("", GroupsRoutes::myEvents); getLoggedIn("/past", GroupsRoutes::myPastEvents); getLoggedIn("/create", GroupsRoutes::createEvent); + getLoggedIn("/calendar", GroupsRoutes::eventCalendar); + getLoggedIn("/dayEventsPopup", GroupsRoutes::eventCalendarDayPopup); }); }); diff --git a/src/main/java/smithereen/Utils.java b/src/main/java/smithereen/Utils.java index 3c2fc369..c91da69d 100644 --- a/src/main/java/smithereen/Utils.java +++ b/src/main/java/smithereen/Utils.java @@ -395,6 +395,10 @@ public static boolean isAjax(Request req){ return req.queryParams("_ajax")!=null; } + public static boolean isMobile(Request req){ + return req.attribute("mobile")!=null; + } + public static String escapeHTML(String s){ return HtmlEscape.escapeHtml4Xml(s); } diff --git a/src/main/java/smithereen/controllers/GroupsController.java b/src/main/java/smithereen/controllers/GroupsController.java index ae847025..a5fe4e5b 100644 --- a/src/main/java/smithereen/controllers/GroupsController.java +++ b/src/main/java/smithereen/controllers/GroupsController.java @@ -8,11 +8,9 @@ import java.sql.SQLException; import java.time.Instant; import java.time.LocalDate; -import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; -import java.time.temporal.TemporalUnit; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -290,7 +288,7 @@ public EventReminder getUserEventReminder(@NotNull User user, @NotNull ZoneId ti } } try{ - List events=GroupStorage.getUpcomingEvents(user.id); + List events=GroupStorage.getUserEventsInTimeRange(user.id, Instant.now(), Instant.now().plus(2, ChronoUnit.DAYS)); EventReminder reminder=new EventReminder(); reminder.createdAt=Instant.now(); if(events.isEmpty()){ @@ -332,6 +330,26 @@ public EventReminder getUserEventReminder(@NotNull User user, @NotNull ZoneId ti } } + public List getUserEventsInMonth(@NotNull User user, int year, int month, @NotNull ZoneId timeZone){ + try{ + ZonedDateTime start=ZonedDateTime.of(year, month, 1, 0, 0, 0, 0, timeZone); + int numDays=LocalDate.of(year, month, 1).lengthOfMonth(); + ZonedDateTime end=ZonedDateTime.of(year, month, numDays, 23, 59, 59, 0, timeZone); + return GroupStorage.getUserEventsInTimeRange(user.id, start.toInstant(), end.toInstant()); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public List getUserEventsOnDay(@NotNull User user, @NotNull LocalDate day, @NotNull ZoneId timeZone){ + try{ + Instant start=day.atStartOfDay(timeZone).toInstant(); + return GroupStorage.getUserEventsInTimeRange(user.id, start, start.plusMillis(24*3600_000)); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + public enum EventsType{ FUTURE, PAST, diff --git a/src/main/java/smithereen/controllers/UsersController.java b/src/main/java/smithereen/controllers/UsersController.java index 6d42701c..e0b26e2b 100644 --- a/src/main/java/smithereen/controllers/UsersController.java +++ b/src/main/java/smithereen/controllers/UsersController.java @@ -1,6 +1,12 @@ package smithereen.controllers; import java.sql.SQLException; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import smithereen.ApplicationContext; import smithereen.data.ForeignUser; @@ -35,4 +41,33 @@ public User getLocalUserOrThrow(int id){ throw new ObjectNotFoundException("err_user_not_found"); return user; } + + public List getFriendsWithBirthdaysWithinTwoDays(User self, LocalDate date){ + try{ + ArrayList today=new ArrayList<>(), tomorrow=new ArrayList<>(); + UserStorage.getFriendIdsWithBirthdaysTodayAndTomorrow(self.id, date, today, tomorrow); + if(today.isEmpty() && tomorrow.isEmpty()) + return Collections.emptyList(); + today.addAll(tomorrow); + return UserStorage.getByIdAsList(today); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public List getFriendsWithBirthdaysInMonth(User self, int month){ + try{ + return UserStorage.getByIdAsList(UserStorage.getFriendsWithBirthdaysInMonth(self.id, month)); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } + + public List getFriendsWithBirthdaysOnDay(User self, int month, int day){ + try{ + return UserStorage.getByIdAsList(UserStorage.getFriendsWithBirthdaysOnDay(self.id, month, day)); + }catch(SQLException x){ + throw new InternalServerErrorException(x); + } + } } diff --git a/src/main/java/smithereen/data/ActorWithDescription.java b/src/main/java/smithereen/data/ActorWithDescription.java new file mode 100644 index 00000000..42bb953c --- /dev/null +++ b/src/main/java/smithereen/data/ActorWithDescription.java @@ -0,0 +1,6 @@ +package smithereen.data; + +import smithereen.activitypub.objects.Actor; + +public record ActorWithDescription(Actor actor, String description){ +} diff --git a/src/main/java/smithereen/data/ForeignUser.java b/src/main/java/smithereen/data/ForeignUser.java index 7174a619..7251306c 100644 --- a/src/main/java/smithereen/data/ForeignUser.java +++ b/src/main/java/smithereen/data/ForeignUser.java @@ -6,6 +6,7 @@ import java.sql.Date; import java.sql.ResultSet; import java.sql.SQLException; +import java.time.LocalDate; import smithereen.Utils; import smithereen.activitypub.ParserContext; @@ -105,7 +106,7 @@ protected ActivityPubObject parseActivityPubObject(JsonObject obj, ParserContext firstName=StringUtils.isNotEmpty(name) ? name : username; } if(obj.has("vcard:bday")){ - birthDate=Date.valueOf(obj.get("vcard:bday").getAsString()); + birthDate=LocalDate.parse(obj.get("vcard:bday").getAsString()); } if(obj.has("gender")){ gender=switch(obj.get("gender").getAsString()){ diff --git a/src/main/java/smithereen/data/User.java b/src/main/java/smithereen/data/User.java index e3b9f106..d306ff21 100644 --- a/src/main/java/smithereen/data/User.java +++ b/src/main/java/smithereen/data/User.java @@ -8,6 +8,7 @@ import java.net.URI; import java.sql.ResultSet; import java.sql.SQLException; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Map; @@ -17,6 +18,7 @@ import smithereen.activitypub.objects.Actor; import smithereen.activitypub.objects.PropertyValue; import smithereen.jsonld.JLD; +import smithereen.storage.DatabaseUtils; import spark.utils.StringUtils; public class User extends Actor{ @@ -28,7 +30,7 @@ public class User extends Actor{ public String lastName; public String middleName; public String maidenName; - public java.sql.Date birthDate; + public LocalDate birthDate; public Gender gender; public long flags; @@ -117,7 +119,7 @@ protected void fillFromResultSet(ResultSet res) throws SQLException{ lastName=res.getString("lname"); middleName=res.getString("middle_name"); maidenName=res.getString("maiden_name"); - birthDate=res.getDate("bdate"); + birthDate=DatabaseUtils.getLocalDate(res, "bdate"); gender=Gender.valueOf(res.getInt("gender")); summary=res.getString("about"); flags=res.getLong("flags"); diff --git a/src/main/java/smithereen/lang/Lang.java b/src/main/java/smithereen/lang/Lang.java index 957d83fb..b98ee4fc 100644 --- a/src/main/java/smithereen/lang/Lang.java +++ b/src/main/java/smithereen/lang/Lang.java @@ -15,6 +15,7 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.LocalDate; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; @@ -187,7 +188,7 @@ public User.Gender detectGenderForName(String first, String last, String middle) } public String formatDay(LocalDate date){ - return get("date_format_other_year", Map.of("day", date.getDayOfMonth(), "month", get("month"+date.getMonthValue()+"_full"), "year", date.getYear())); + return get("date_format_other_year", Map.of("day", date.getDayOfMonth(), "month", get("month_full", Map.of("month", date.getMonthValue())), "year", date.getYear())); } public String formatDate(Instant date, TimeZone timeZone, boolean forceAbsolute){ @@ -223,15 +224,20 @@ public String formatDate(Instant date, TimeZone timeZone, boolean forceAbsolute) } if(day==null){ if(now.getYear()==dt.getYear()){ - day=get("date_format_current_year", Map.of("day", dt.getDayOfMonth(), "month", get("month"+dt.getMonthValue()+"_full"))); + day=get("date_format_current_year", Map.of("day", dt.getDayOfMonth(), "month", get("month_full", Map.of("month", dt.getMonthValue())))); }else{ - day=get("date_format_other_year", Map.of("day", dt.getDayOfMonth(), "month", get("month"+dt.getMonthValue()+"_short"), "year", dt.getYear())); + day=get("date_format_other_year", Map.of("day", dt.getDayOfMonth(), "month", get("month_short", Map.of("month", dt.getMonthValue())), "year", dt.getYear())); } } return get("date_time_format", Map.of("date", day, "time", String.format(locale, "%d:%02d", dt.getHour(), dt.getMinute()))); } + public String formatTime(Instant time, ZoneId timeZone){ + ZonedDateTime dt=time.atZone(timeZone); + return String.format(locale, "%d:%02d", dt.getHour(), dt.getMinute()); + } + public String getAsJS(String key){ if(!data.containsKey(key)){ if(fallback!=null) diff --git a/src/main/java/smithereen/routes/GroupsRoutes.java b/src/main/java/smithereen/routes/GroupsRoutes.java index 93aae44f..f612406b 100644 --- a/src/main/java/smithereen/routes/GroupsRoutes.java +++ b/src/main/java/smithereen/routes/GroupsRoutes.java @@ -9,25 +9,23 @@ import java.sql.SQLException; import java.time.Instant; import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.format.DateTimeFormatter; +import java.time.ZoneId; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.TimeZone; +import java.util.Objects; import java.util.stream.Collectors; +import smithereen.activitypub.objects.Actor; import smithereen.activitypub.objects.PropertyValue; import smithereen.controllers.GroupsController; +import smithereen.data.ActorWithDescription; import smithereen.data.ForeignUser; import smithereen.data.PaginatedList; import smithereen.data.SizedImage; -import smithereen.data.feed.NewsfeedEntry; import smithereen.exceptions.BadRequestException; import smithereen.Config; import smithereen.data.GroupAdmin; @@ -42,11 +40,8 @@ import smithereen.data.User; import smithereen.data.UserInteractions; import smithereen.data.WebDeltaResponse; -import smithereen.exceptions.UserActionNotAllowedException; import smithereen.lang.Lang; import smithereen.storage.GroupStorage; -import smithereen.storage.NewsfeedStorage; -import smithereen.storage.PostStorage; import smithereen.storage.UserStorage; import smithereen.templates.RenderedTemplateResponse; import spark.Request; @@ -332,8 +327,8 @@ private static Object members(Request req, Response resp, boolean tentative){ public static Object admins(Request req, Response resp){ Group group=getGroup(req); - RenderedTemplateResponse model=new RenderedTemplateResponse("group_admins", req); - model.with("admins", context(req).getGroupsController().getAdmins(group)); + RenderedTemplateResponse model=new RenderedTemplateResponse("actor_list", req); + model.with("actors", context(req).getGroupsController().getAdmins(group).stream().map(a->new ActorWithDescription(a.user, a.title)).collect(Collectors.toList())); if(isAjax(req)){ return new WebDeltaResponse(resp).box(lang(req).get(group.isEvent() ? "event_organizers" : "group_admins"), model.renderContentBlock(), null, true); } @@ -534,4 +529,199 @@ public static Object unblockUser(Request req, Response resp, Account self) throw resp.redirect(back(req)); return ""; } + +// public static Object eventCalendar(Request req, Response resp, Account self){ +// if(isMobile(req)) +// return eventCalendarMobile(req, resp, self); +// else +// return eventCalendarDesktop(req, resp, self); +// } + + public static Object eventCalendar(Request req, Response resp, Account self){ + ZoneId timeZone=timeZoneForRequest(req).toZoneId(); + LocalDate today=LocalDate.now(timeZone); + LocalDate tomorrow=today.plusDays(1); + RenderedTemplateResponse model=new RenderedTemplateResponse(isAjax(req) ? "events_actual_calendar" : "events_calendar", req); + + if(!isAjax(req) && !isMobile(req)){ + List birthdays=context(req).getUsersController().getFriendsWithBirthdaysWithinTwoDays(self.user, today); + model.with("birthdays", birthdays); + if(!birthdays.isEmpty()){ + HashMap days=new HashMap<>(birthdays.size()), ages=new HashMap<>(birthdays.size()); + for(User user : birthdays){ + days.put(user.id, lang(req).get(today.getDayOfMonth()==user.birthDate.getDayOfMonth() && today.getMonthValue()==user.birthDate.getMonthValue() ? "date_today" : "date_tomorrow")); + LocalDate birthday=user.birthDate.withYear(today.getYear()); + if(birthday.isBefore(today)) + birthday=birthday.plusYears(1); + ages.put(user.id, lang(req).get("X_years", Map.of("count", birthday.getYear()-user.birthDate.getYear()))); + } + model.with("userDays", days).with("userAges", ages); + } + PaginatedList events=context(req).getGroupsController().getUserEvents(self.user, GroupsController.EventsType.FUTURE, 0, 10); + Instant eventMaxTime=tomorrow.atTime(23, 59, 59).atZone(timeZone).toInstant(); + List eventsWithinTwoDays=events.list.stream().filter(e->e.eventStartTime.isBefore(eventMaxTime)).toList(); + model.with("events", eventsWithinTwoDays); + } + + int month=safeParseInt(req.queryParams("month")); + int year=safeParseInt(req.queryParams("year")); + LocalDate monthStart; + if(month<1 || month>12 || year==0){ + monthStart=LocalDate.now(timeZone).withDayOfMonth(1); + month=monthStart.getMonthValue(); + year=monthStart.getYear(); + }else{ + monthStart=LocalDate.of(year, month, 1); + } + model.with("year", year).with("month", month).with("monthLength", monthStart.lengthOfMonth()).with("monthStartWeekday", monthStart.getDayOfWeek().getValue()); + model.with("todayDay", today.getDayOfMonth()).with("todayMonth", today.getMonthValue()).with("todayYear", today.getYear()); + Lang l=lang(req); + Instant now=Instant.now(); + + ArrayList eventsInMonth=new ArrayList<>(); + eventsInMonth.addAll(context(req).getUsersController().getFriendsWithBirthdaysInMonth(self.user, month)); + eventsInMonth.addAll(context(req).getGroupsController().getUserEventsInMonth(self.user, year, month, timeZone)); + if(isMobile(req)){ + Map> eventsByDay=eventsInMonth.stream() + .map(a->new ActorWithDescription(a, getActorCalendarDescription(a, l, today, monthStart, now, timeZone))) + .collect(Collectors.groupingBy(a->{ + if(a.actor() instanceof User u) + return u.birthDate.getDayOfMonth(); + else if(a.actor() instanceof Group g) + return g.eventStartTime.atZone(timeZone).getDayOfMonth(); + else + throw new IllegalStateException(); + })); + model.with("calendarEvents", eventsByDay); + }else{ + Map> eventsByDay=eventsInMonth.stream().collect(Collectors.groupingBy(a->{ + if(a instanceof User u) + return u.birthDate.getDayOfMonth(); + else if(a instanceof Group g) + return g.eventStartTime.atZone(timeZone).getDayOfMonth(); + else + throw new IllegalStateException(); + })); + model.with("calendarEvents", eventsByDay); + } + model.pageTitle(lang(req).get("events_calendar_title")); + + if(isAjax(req)){ + return new WebDeltaResponse(resp).setContent("eventsCalendarW", model.renderToString()); + } + + return model; + } + + public static Object eventCalendarMobile(Request req, Response resp, Account self){ + RenderedTemplateResponse model=new RenderedTemplateResponse("events_calendar", req); + Lang l=lang(req); + Instant now=Instant.now(); + ZoneId timeZone=timeZoneForRequest(req).toZoneId(); + LocalDate today=LocalDate.now(timeZone); + int month=safeParseInt(req.queryParams("month")); + int year=safeParseInt(req.queryParams("year")); + LocalDate monthStart; + if(month<1 || month>12 || year==0){ + monthStart=LocalDate.now(timeZone).withDayOfMonth(1); + month=monthStart.getMonthValue(); + year=monthStart.getYear(); + }else{ + monthStart=LocalDate.of(year, month, 1); + } + model.with("month", month).with("year", year); + ArrayList eventsInMonth=new ArrayList<>(); + eventsInMonth.addAll(context(req).getUsersController().getFriendsWithBirthdaysInMonth(self.user, month)); + eventsInMonth.addAll(context(req).getGroupsController().getUserEventsInMonth(self.user, year, month, timeZone)); + List actors=eventsInMonth.stream().sorted((a1, a2)->{ + LocalDate date1, date2; + if(a1 instanceof User u) + date1=u.birthDate; + else if(a1 instanceof Group g) + date1=g.eventStartTime.atZone(timeZone).toLocalDate(); + else + throw new IllegalStateException(); + if(a2 instanceof User u) + date2=u.birthDate; + else if(a2 instanceof Group g) + date2=g.eventStartTime.atZone(timeZone).toLocalDate(); + else + throw new IllegalStateException(); + if(date1.equals(date2)){ + if(a1 instanceof User && a2 instanceof Group){ + return -1; + }else if(a1 instanceof Group && a2 instanceof User){ + return 1; + }else if(a1 instanceof User u1 && a2 instanceof User u2){ + return Integer.compare(u1.id, u2.id); + }else if(a1 instanceof Group g1 && a2 instanceof Group g2){ + return g1.eventStartTime.compareTo(g2.eventStartTime); + }else{ + throw new IllegalStateException(); + } + }else{ + return date1.compareTo(date2); + } + }).map(a->new ActorWithDescription(a, getActorCalendarDescription(a, l, today, monthStart, now, timeZone))).toList(); + model.with("actors", actors); + model.pageTitle(lang(req).get("events_calendar_title")); + + return model; + } + + public static Object eventCalendarDayPopup(Request req, Response resp, Account self){ + LocalDate date; + try{ + date=LocalDate.parse(Objects.requireNonNullElse(req.queryParams("date"), "")); + }catch(DateTimeParseException x){ + throw new BadRequestException(x); + } + + Lang l=lang(req); + ZoneId timeZone=timeZoneForRequest(req).toZoneId(); + Instant now=Instant.now(); + LocalDate today=LocalDate.now(timeZone); + RenderedTemplateResponse model=new RenderedTemplateResponse("actor_list", req); + ArrayList actors=new ArrayList<>(); + actors.addAll(context(req).getUsersController().getFriendsWithBirthdaysOnDay(self.user, date.getMonthValue(), date.getDayOfMonth())); + actors.addAll(context(req).getGroupsController().getUserEventsOnDay(self.user, date, timeZone)); + List actorsDescr=actors.stream().sorted((a1, a2)->{ + if(a1 instanceof User && a2 instanceof Group){ + return -1; + }else if(a1 instanceof Group && a2 instanceof User){ + return 1; + }else if(a1 instanceof User u1 && a2 instanceof User u2){ + return Integer.compare(u1.id, u2.id); + }else if(a1 instanceof Group g1 && a2 instanceof Group g2){ + return g1.eventStartTime.compareTo(g2.eventStartTime); + } + return 0; + }).map(a->new ActorWithDescription(a, getActorCalendarDescription(a, l, today, date, now, timeZone))).toList(); + model.with("actors", actorsDescr); + if(isAjax(req)){ + return new WebDeltaResponse(resp).box(l.get("events_for_date", Map.of("date", l.formatDay(date))), model.renderContentBlock(), null, true); + } + return model; + } + + private static String getActorCalendarDescription(Actor a, Lang l, LocalDate today, LocalDate targetDate, Instant now, ZoneId timeZone){ + if(a instanceof User u){ + LocalDate birthday=u.birthDate.withYear(targetDate.getYear()); + if(birthday.isBefore(targetDate)) + birthday=birthday.plusYears(1); + int age=birthday.getYear()-u.birthDate.getYear(); + String key; + if(targetDate.isBefore(today)) + key="birthday_descr_past"; + else if(targetDate.isAfter(today)) + key="birthday_descr_future"; + else + key="birthday_descr_today"; + return l.get(key, Map.of("age", age)); + }else if(a instanceof Group g){ + return l.get(g.eventStartTime.isBefore(now) ? "event_descr_past" : "event_descr_future", Map.of("time", l.formatTime(g.eventStartTime, timeZone))); + }else{ + throw new IllegalStateException(); + } + } } diff --git a/src/main/java/smithereen/routes/ProfileRoutes.java b/src/main/java/smithereen/routes/ProfileRoutes.java index c98d2e63..80064daa 100644 --- a/src/main/java/smithereen/routes/ProfileRoutes.java +++ b/src/main/java/smithereen/routes/ProfileRoutes.java @@ -82,7 +82,7 @@ public static Object profile(Request req, Response resp) throws SQLException{ ArrayList profileFields=new ArrayList<>(); if(user.birthDate!=null) - profileFields.add(new PropertyValue(l.get("birth_date"), l.formatDay(user.birthDate.toLocalDate()))); + profileFields.add(new PropertyValue(l.get("birth_date"), l.formatDay(user.birthDate))); if(StringUtils.isNotEmpty(user.summary)) profileFields.add(new PropertyValue(l.get("profile_about"), user.summary)); if(user.attachment!=null) diff --git a/src/main/java/smithereen/routes/SettingsRoutes.java b/src/main/java/smithereen/routes/SettingsRoutes.java index f4bf6dc3..bd673ed8 100644 --- a/src/main/java/smithereen/routes/SettingsRoutes.java +++ b/src/main/java/smithereen/routes/SettingsRoutes.java @@ -11,6 +11,8 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.sql.SQLException; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; import java.util.List; import java.util.Locale; import java.util.Map; @@ -123,18 +125,12 @@ public static Object updateProfileGeneral(Request req, Response resp, Account se if(_gender<0 || _gender>2) _gender=0; User.Gender gender=User.Gender.valueOf(_gender); - java.sql.Date bdate=self.user.birthDate; + LocalDate bdate=self.user.birthDate; String _bdate=req.queryParams("bdate"); if(_bdate!=null){ - String[] dateParts=_bdate.split("-"); - if(dateParts.length==3){ - int year=parseIntOrDefault(dateParts[0], 0); - int month=parseIntOrDefault(dateParts[1], 0); - int day=parseIntOrDefault(dateParts[2], 0); - if(year>=1900 && year<9999 && month>0 && month<=12 && day>0 && day<=31){ - bdate=new java.sql.Date(year-1900, month-1, day); - } - } + try{ + bdate=LocalDate.parse(_bdate); + }catch(DateTimeParseException ignore){} } String message; if(first.length()<2){ diff --git a/src/main/java/smithereen/storage/DatabaseUtils.java b/src/main/java/smithereen/storage/DatabaseUtils.java index e8f24f60..992e02b9 100644 --- a/src/main/java/smithereen/storage/DatabaseUtils.java +++ b/src/main/java/smithereen/storage/DatabaseUtils.java @@ -154,6 +154,11 @@ public static LocalDate getLocalDate(ResultSet res, String name) throws SQLExcep return date==null ? null : date.toLocalDate(); } + public static LocalDate getLocalDate(ResultSet res, int index) throws SQLException{ + Date date=res.getDate(index); + return date==null ? null : date.toLocalDate(); + } + public static void doWithTransaction(Connection conn, SQLRunnable r) throws SQLException{ boolean success=false; try{ diff --git a/src/main/java/smithereen/storage/GroupStorage.java b/src/main/java/smithereen/storage/GroupStorage.java index 75195605..34e078bb 100644 --- a/src/main/java/smithereen/storage/GroupStorage.java +++ b/src/main/java/smithereen/storage/GroupStorage.java @@ -883,9 +883,9 @@ static String getQSearchStringForGroup(Group group){ return s; } - public static List getUpcomingEvents(int userID) throws SQLException{ + public static List getUserEventsInTimeRange(int userID, Instant from, Instant to) throws SQLException{ Connection conn=DatabaseConnectionManager.getConnection(); - PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT group_id, event_start_time FROM group_memberships JOIN `groups` ON `groups`.id=group_memberships.group_id WHERE user_id=? AND accepted=1 AND `groups`.type=1 AND event_start_time>NOW() AND event_start_time=? AND event_start_time)arg).ordinal()); + if(arg instanceof Enum e) + stmt.setInt(i, e.ordinal()); + else if(arg instanceof Instant instant) + stmt.setTimestamp(i, Timestamp.from(instant)); + else if(arg instanceof LocalDate ld) + stmt.setDate(i, java.sql.Date.valueOf(ld)); else stmt.setObject(i, arg); i++; diff --git a/src/main/java/smithereen/storage/UserStorage.java b/src/main/java/smithereen/storage/UserStorage.java index 30c3f8c6..12196f71 100644 --- a/src/main/java/smithereen/storage/UserStorage.java +++ b/src/main/java/smithereen/storage/UserStorage.java @@ -552,7 +552,7 @@ public static void putInvite(int userID, byte[] code, int signups) throws SQLExc stmt.execute(); } - public static void changeBasicInfo(User user, String firstName, String lastName, String middleName, String maidenName, User.Gender gender, java.sql.Date bdate, String about) throws SQLException{ + public static void changeBasicInfo(User user, String firstName, String lastName, String middleName, String maidenName, User.Gender gender, LocalDate bdate, String about) throws SQLException{ new SQLQueryBuilder() .update("users") .where("id=?", user.id) @@ -1011,32 +1011,38 @@ public static void removeBirthdayReminderFromCache(List userIDs){ } } - public static BirthdayReminder getBirthdayReminderForUser(int userID, LocalDate date) throws SQLException{ - synchronized(birthdayReminderCache){ - BirthdayReminder r=birthdayReminderCache.get(userID); - if(r!=null && r.forDay.equals(date)) - return r; - } + public static void getFriendIdsWithBirthdaysTodayAndTomorrow(int userID, LocalDate date, List today, List tomorrow) throws SQLException{ LocalDate nextDay=date.plusDays(1); Connection conn=DatabaseConnectionManager.getConnection(); PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT `users`.id, `users`.bdate FROM `users` RIGHT JOIN followings ON followings.followee_id=`users`.id" + - " WHERE followings.follower_id=? AND followings.mutual=1 AND `users`.bdate IS NOT NULL" + - " AND ((DAY(`users`.bdate)=? AND MONTH(`users`.bdate)=?) OR (DAY(`users`.bdate)=? AND MONTH(`users`.bdate)=?))", + " WHERE followings.follower_id=? AND followings.mutual=1 AND `users`.bdate IS NOT NULL" + + " AND ((DAY(`users`.bdate)=? AND MONTH(`users`.bdate)=?) OR (DAY(`users`.bdate)=? AND MONTH(`users`.bdate)=?))", userID, date.getDayOfMonth(), date.getMonthValue(), nextDay.getDayOfMonth(), nextDay.getMonthValue()); - List today=new ArrayList<>(), tomorrow=new ArrayList<>(); try(ResultSet res=stmt.executeQuery()){ res.beforeFirst(); while(res.next()){ int id=res.getInt(1); - Date bdate=res.getDate(2); - if(bdate.getDate()==date.getDayOfMonth()){ + LocalDate bdate=DatabaseUtils.getLocalDate(res, 2); + Objects.requireNonNull(bdate); + if(bdate.getDayOfMonth()==date.getDayOfMonth()){ today.add(id); }else{ tomorrow.add(id); } } } + } + + public static BirthdayReminder getBirthdayReminderForUser(int userID, LocalDate date) throws SQLException{ + synchronized(birthdayReminderCache){ + BirthdayReminder r=birthdayReminderCache.get(userID); + if(r!=null && r.forDay.equals(date)) + return r; + } + LocalDate nextDay=date.plusDays(1); + ArrayList today=new ArrayList<>(), tomorrow=new ArrayList<>(); + getFriendIdsWithBirthdaysTodayAndTomorrow(userID, date, today, tomorrow); BirthdayReminder r=new BirthdayReminder(); r.forDay=date; if(!today.isEmpty()){ @@ -1053,4 +1059,18 @@ public static BirthdayReminder getBirthdayReminderForUser(int userID, LocalDate return r; } } + + public static List getFriendsWithBirthdaysInMonth(int userID, int month) throws SQLException{ + Connection conn=DatabaseConnectionManager.getConnection(); + PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT `users`.id FROM `users` RIGHT JOIN followings ON followings.followee_id=`users`.id" + + " WHERE followings.follower_id=? AND followings.mutual=1 AND `users`.bdate IS NOT NULL AND MONTH(`users`.bdate)=?", userID, month); + return DatabaseUtils.intResultSetToList(stmt.executeQuery()); + } + + public static List getFriendsWithBirthdaysOnDay(int userID, int month, int day) throws SQLException{ + Connection conn=DatabaseConnectionManager.getConnection(); + PreparedStatement stmt=SQLQueryBuilder.prepareStatement(conn, "SELECT `users`.id FROM `users` RIGHT JOIN followings ON followings.followee_id=`users`.id" + + " WHERE followings.follower_id=? AND followings.mutual=1 AND `users`.bdate IS NOT NULL AND MONTH(`users`.bdate)=? AND DAY(`users`.bdate)=?", userID, month, day); + return DatabaseUtils.intResultSetToList(stmt.executeQuery()); + } } diff --git a/src/main/java/smithereen/templates/PictureForAvatarFilter.java b/src/main/java/smithereen/templates/PictureForAvatarFilter.java index 64056a50..e5b36767 100644 --- a/src/main/java/smithereen/templates/PictureForAvatarFilter.java +++ b/src/main/java/smithereen/templates/PictureForAvatarFilter.java @@ -49,8 +49,11 @@ else if(actor instanceof Group) typeStr=typeStr.substring(1); if(args.containsKey("size")) size=Templates.asInt(args.get("size")); - if(image==null) - return new SafeString(""); + if(image==null){ + if(args.containsKey("wrapperClasses")) + additionalClasses+=" "+args.get("wrapperClasses").toString(); + return new SafeString("0 ? (" style=\"width: "+size+"px;height: "+size+"px\"") : "")+">"); + } int width, height; if(isRect){ @@ -66,11 +69,11 @@ else if(actor instanceof Group) if(args.containsKey("classes")){ classes.add(args.get("classes").toString()); } - return new SafeString(""+image.generateHTML(type, classes, null, width, height)+""); + return new SafeString(""+image.generateHTML(type, classes, null, width, height)+""); } @Override public List getArgumentNames(){ - return Arrays.asList("type", "size", "classes"); + return Arrays.asList("type", "size", "classes", "wrapperClasses"); } } diff --git a/src/main/resources/langs/de.json b/src/main/resources/langs/de.json index 465a2721..945b8801 100644 --- a/src/main/resources/langs/de.json +++ b/src/main/resources/langs/de.json @@ -284,42 +284,9 @@ "date_today": "Heute", "date_yesterday": "Gestern", "date_tomorrow": "Morgen", - "month1_full": "Januar", - "month2_full": "Februar", - "month3_full": "März", - "month4_full": "April", - "month5_full": "Mai", - "month6_full": "Juni", - "month7_full": "Juli", - "month8_full": "August", - "month9_full": "September", - "month10_full": "Oktober", - "month11_full": "November", - "month12_full": "Dezember", - "month1_short": "Jan", - "month2_short": "Feb", - "month3_short": "Mär", - "month4_short": "Apr", - "month5_short": "Mai", - "month6_short": "Jun", - "month7_short": "Jul", - "month8_short": "Aug", - "month9_short": "Sep", - "month10_short": "Okt", - "month11_short": "Nov", - "month12_short": "Dez", - "month1_standalone": "Januar", - "month2_standalone": "Februar", - "month3_standalone": "März", - "month4_standalone": "April", - "month5_standalone": "Mai", - "month6_standalone": "Juni", - "month7_standalone": "Juli", - "month8_standalone": "August", - "month9_standalone": "September", - "month10_standalone": "Oktober", - "month11_standalone": "November", - "month12_standalone": "Dezember", + "month_full": "{month, select, 1 {Januar} 2 {Februar} 3 {März} 4 {April} 5 {Mai} 6 {Juni} 7 {Juli} 8 {August} 9 {September} 10 {Oktober} 11 {November} 12 {Dezember} other {?}}", + "month_short": "{month, select, 1 {Jan} 2 {Feb} 3 {Mär} 4 {Apr} 5 {Mai} 6 {Jun} 7 {Jul} 8 {Aug} 9 {Sep} 10 {Okt} 11 {Nov} 12 {Dez} other {?}}", + "month_standalone": "{month, select, 1 {Januar} 2 {Februar} 3 {März} 4 {April} 5 {Mai} 6 {Juni} 7 {Juli} 8 {August} 9 {September} 10 {Oktober} 11 {November} 12 {Dezember} other {?}}", "date_time_format": "{date} at {time}", "date_format_current_year": "{day} {month}", "date_format_other_year": "{day} {month} {year}", diff --git a/src/main/resources/langs/en.json b/src/main/resources/langs/en.json index a1e51999..a0241678 100644 --- a/src/main/resources/langs/en.json +++ b/src/main/resources/langs/en.json @@ -281,42 +281,9 @@ "date_today": "today", "date_yesterday": "yesterday", "date_tomorrow": "tomorrow", - "month1_full": "January", - "month2_full": "February", - "month3_full": "March", - "month4_full": "April", - "month5_full": "May", - "month6_full": "June", - "month7_full": "July", - "month8_full": "August", - "month9_full": "September", - "month10_full": "October", - "month11_full": "November", - "month12_full": "December", - "month1_short": "Jan", - "month2_short": "Feb", - "month3_short": "Mar", - "month4_short": "Apr", - "month5_short": "May", - "month6_short": "Jun", - "month7_short": "Jul", - "month8_short": "Aug", - "month9_short": "Sep", - "month10_short": "Oct", - "month11_short": "Nov", - "month12_short": "Dec", - "month1_standalone": "January", - "month2_standalone": "February", - "month3_standalone": "March", - "month4_standalone": "April", - "month5_standalone": "May", - "month6_standalone": "June", - "month7_standalone": "July", - "month8_standalone": "August", - "month9_standalone": "September", - "month10_standalone": "October", - "month11_standalone": "November", - "month12_standalone": "December", + "month_full": "{month, select, 1 {January} 2 {February} 3 {March} 4 {April} 5 {May} 6 {June} 7 {July} 8 {August} 9 {September} 10 {October} 11 {November} 12 {December} other {?}}", + "month_short": "{month, select, 1 {Jan} 2 {Feb} 3 {Mar} 4 {Apr} 5 {May} 6 {Jun} 7 {Jul} 8 {Aug} 9 {Sep} 10 {Oct} 11 {Nov} 12 {Dec} other {?}}", + "month_standalone": "{month, select, 1 {January} 2 {February} 3 {March} 4 {April} 5 {May} 6 {June} 7 {July} 8 {August} 9 {September} 10 {October} 11 {November} 12 {December} other {?}}", "date_time_format": "{date} at {time}", "date_format_current_year": "{day} {month}", "date_format_other_year": "{day} {month} {year}", @@ -495,5 +462,21 @@ "event_reminder_day_pos": "end", "birthday_reminder_day_pos": "start", "event_reminder_today": "today", - "event_reminder_tomorrow": "tomorrow" + "event_reminder_tomorrow": "tomorrow", + "events_calendar_summary": "Upcoming events", + "group_size": "Size", + "X_years": "{count, plural, one {# year} other {# years}}", + "birthday": "Birthday", + "birthday_turns": "Turns", + "date_format_month_year": "{month} {year}", + "weekday_short": "{day, select, 1 {Mon} 2 {Tue} 3 {Wed} 4 {Thu} 5 {Fri} 6 {Sat} 7 {Sun} other {?}}", + "events_calendar_title": "Friends' birthdays and events", + "show_more": "Show more", + "event_descr_past": "Event took place at {time}", + "event_descr_future": "Event will take place at {time}", + "birthday_descr_past": "Turned {age, plural, one {# year} other {# years}}", + "birthday_descr_today": "Turns {age, plural, one {# year} other {# years}}", + "birthday_descr_future": "Will turn {age, plural, one {# year} other {# years}}", + "events_for_date": "Events for {date}", + "no_events_this_month": "There are no events this month" } \ No newline at end of file diff --git a/src/main/resources/langs/es.json b/src/main/resources/langs/es.json index bad96039..e6e68506 100644 --- a/src/main/resources/langs/es.json +++ b/src/main/resources/langs/es.json @@ -284,42 +284,9 @@ "date_today": "hoy", "date_yesterday": "ayer", "date_tomorrow": "mañana", - "month1_full": "Enero", - "month2_full": "Febrero", - "month3_full": "Marzo", - "month4_full": "Abril", - "month5_full": "Mayo", - "month6_full": "Junio", - "month7_full": "Julio", - "month8_full": "Agosto", - "month9_full": "Septiembre", - "month10_full": "Octubre", - "month11_full": "Noviembre", - "month12_full": "Diciembre", - "month1_short": "Ene", - "month2_short": "Feb", - "month3_short": "Mar", - "month4_short": "Abr", - "month5_short": "May", - "month6_short": "Jun", - "month7_short": "Jul", - "month8_short": "Ago", - "month9_short": "Sep", - "month10_short": "Oct", - "month11_short": "Nov", - "month12_short": "Dic", - "month1_standalone": "Enero", - "month2_standalone": "Febrero", - "month3_standalone": "Marzo", - "month4_standalone": "Abril", - "month5_standalone": "Mayo", - "month6_standalone": "Junio", - "month7_standalone": "Julio", - "month8_standalone": "Agosto", - "month9_standalone": "Septiembre", - "month10_standalone": "Octubre", - "month11_standalone": "Noviembre", - "month12_standalone": "Diciembre", + "month_full": "{month, select, 1 {Enero} 2 {Febrero} 3 {Marzo} 4 {Abril} 5 {Mayo} 6 {Junio} 7 {Julio} 8 {Agosto} 9 {Septiembre} 10 {Octubre} 11 {Noviembre} 12 {Diciembre} other {?}}", + "month_short": "{month, select, 1 {Ene} 2 {Feb} 3 {Mar} 4 {Abr} 5 {May} 6 {Jun} 7 {Jul} 8 {Ago} 9 {Sep} 10 {Oct} 11 {Nov} 12 {Dic} other {?}}", + "month_standalone": "{month, select, 1 {Enero} 2 {Febrero} 3 {Marzo} 4 {Abril} 5 {Mayo} 6 {Junio} 7 {Julio} 8 {Agosto} 9 {Septiembre} 10 {Octubre} 11 {Noviembre} 12 {Diciembre} other {?}}", "date_time_format": "{date} a las {time}", "date_format_current_year": "{day} {month}", "date_format_other_year": "{day} {month} {year}", diff --git a/src/main/resources/langs/pl.json b/src/main/resources/langs/pl.json index cf7acca4..6e5a50fe 100644 --- a/src/main/resources/langs/pl.json +++ b/src/main/resources/langs/pl.json @@ -284,42 +284,9 @@ "date_today": "dzisiaj", "date_yesterday": "wczoraj", "date_tomorrow": "jutro", - "month1_full": "stycznia", - "month2_full": "lutego", - "month3_full": "marca", - "month4_full": "kwietnia", - "month5_full": "maja", - "month6_full": "czerwca", - "month7_full": "lipca", - "month8_full": "sierpnia", - "month9_full": "września", - "month10_full": "października", - "month11_full": "listopada", - "month12_full": "grudnia", - "month1_short": "st.", - "month2_short": "lt.", - "month3_short": "mr.", - "month4_short": "kw.", - "month5_short": "mj.", - "month6_short": "cz.", - "month7_short": "lp.", - "month8_short": "sr.", - "month9_short": "wr.", - "month10_short": "pź.", - "month11_short": "ls.", - "month12_short": "gr.", - "month1_standalone": "styczeń", - "month2_standalone": "luty", - "month3_standalone": "marzec", - "month4_standalone": "kwiecień", - "month5_standalone": "maj", - "month6_standalone": "czerwiec", - "month7_standalone": "lipiec", - "month8_standalone": "sierpień", - "month9_standalone": "wrzesień", - "month10_standalone": "październrik", - "month11_standalone": "listopad", - "month12_standalone": "grudzień", + "month_full": "{month, select, 1 {stycznia} 2 {lutego} 3 {marca} 4 {kwietnia} 5 {maja} 6 {czerwca} 7 {lipca} 8 {sierpnia} 9 {września} 10 {października} 11 {listopada} 12 {grudnia} other {?}}", + "month_short": "{month, select, 1 {st.} 2 {lt.} 3 {mr.} 4 {kw.} 5 {mj.} 6 {cz.} 7 {lp.} 8 {sr.} 9 {wr.} 10 {pź.} 11 {ls.} 12 {gr.} other {?}}", + "month_standalone": "{month, select, 1 {Styczeń} 2 {Luty} 3 {Marzec} 4 {Kwiecień} 5 {Maj} 6 {Czerwiec} 7 {Lipiec} 8 {Sierpień} 9 {Wrzesień} 10 {Październrik} 11 {Listopad} 12 {Grudzień} other {?}}", "date_time_format": "{date} o {time}", "date_format_current_year": "{day} {month}", "date_format_other_year": "{day} {month} {year}", diff --git a/src/main/resources/langs/ru.json b/src/main/resources/langs/ru.json index c51eb1e6..97ec1b1a 100644 --- a/src/main/resources/langs/ru.json +++ b/src/main/resources/langs/ru.json @@ -282,42 +282,9 @@ "date_yesterday": "вчера", "date_tomorrow": "завтра", - "month1_full": "января", - "month2_full": "февраля", - "month3_full": "марта", - "month4_full": "апреля", - "month5_full": "мая", - "month6_full": "июня", - "month7_full": "июля", - "month8_full": "августа", - "month9_full": "сентября", - "month10_full": "октября", - "month11_full": "ноября", - "month12_full": "декабря", - "month1_short": "янв", - "month2_short": "фев", - "month3_short": "мар", - "month4_short": "апр", - "month5_short": "мая", - "month6_short": "июн", - "month7_short": "июл", - "month8_short": "авг", - "month9_short": "сен", - "month10_short": "окт", - "month11_short": "ноя", - "month12_short": "дек", - "month1_standalone": "январь", - "month2_standalone": "февраль", - "month3_standalone": "март", - "month4_standalone": "апрель", - "month5_standalone": "май", - "month6_standalone": "июнь", - "month7_standalone": "июль", - "month8_standalone": "август", - "month9_standalone": "сентябрь", - "month10_standalone": "октябрь", - "month11_standalone": "ноябрь", - "month12_standalone": "декабрь", + "month_full": "{month, select, 1 {января} 2 {февраля} 3 {марта} 4 {апреля} 5 {мая} 6 {июня} 7 {июля} 8 {августа} 9 {сентября} 10 {октября} 11 {ноября} 12 {декабря} other {?}}", + "month_short": "{month, select, 1 {янв} 2 {фев} 3 {мар} 4 {апр} 5 {мая} 6 {июн} 7 {июл} 8 {авг} 9 {сен} 10 {окт} 11 {ноя} 12 {дек} other {?}}", + "month_standalone": "{month, select, 1 {Январь} 2 {Февраль} 3 {Март} 4 {Апрель} 5 {Май} 6 {Июнь} 7 {Июль} 8 {Август} 9 {Сентябрь} 10 {Октябрь} 11 {Ноябрь} 12 {Декабрь} other {?}}", "date_time_format": "{date} в {time}", "date_format_current_year": "{day} {month}", @@ -497,5 +464,21 @@ "event_reminder_day_pos": "start", "birthday_reminder_day_pos": "start", "event_reminder_today": "Сегодня", - "event_reminder_tomorrow": "Завтра" + "event_reminder_tomorrow": "Завтра", + "events_calendar_summary": "Ближайшие мероприятия", + "group_size": "Размер", + "X_years": "{count, plural, one {# год} few {# года} other {# лет}}", + "birthday": "День рождения", + "birthday_turns": "Исполняется", + "date_format_month_year": "{month} {year}", + "weekday_short": "{day, select, 1 {Пн} 2 {Вт} 3 {Ср} 4 {Чт} 5 {Пт} 6 {Сб} 7 {Вс} other {?}}", + "events_calendar_title": "Дни рождения друзей и встречи", + "show_more": "Показать ещё", + "event_descr_past": "Встреча состоялась в {time}", + "event_descr_future": "Встреча состоится в {time}", + "birthday_descr_past": "{age, plural, one {Исполнился # год} few {Исполнилось # года} other {Исполнилось # лет}}", + "birthday_descr_today": "Исполняется {age, plural, one {# год} few {# года} other {# лет}}", + "birthday_descr_future": "Исполнится {age, plural, one {# год} few {# года} other {# лет}}", + "events_for_date": "События на {date}", + "no_events_this_month": "В этом месяце нет никаких событий" } \ No newline at end of file diff --git a/src/main/resources/templates/common/actor_list.twig b/src/main/resources/templates/common/actor_list.twig new file mode 100644 index 00000000..29ad60a7 --- /dev/null +++ b/src/main/resources/templates/common/actor_list.twig @@ -0,0 +1,18 @@ +{% extends "page" %} +{% block content %} + +{% for actor in actors %} + + + + +{% endfor %} +
        + {{ actor.actor | pictureForAvatar('s') }} + + {{ actor.actor.type=="Person" ? actor.actor.completeName : actor.actor.name }} + {% if actor.description is not empty %} +
        {{ actor.description }}
        + {% endif %} +
        +{% endblock %} diff --git a/src/main/resources/templates/common/events_tabbar.twig b/src/main/resources/templates/common/events_tabbar.twig index 6ac489e9..17d24c0b 100644 --- a/src/main/resources/templates/common/events_tabbar.twig +++ b/src/main/resources/templates/common/events_tabbar.twig @@ -1,5 +1,6 @@ diff --git a/src/main/resources/templates/common/group_admins.twig b/src/main/resources/templates/common/group_admins.twig deleted file mode 100644 index 2fa0f860..00000000 --- a/src/main/resources/templates/common/group_admins.twig +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "page" %} -{% block content %} - -{% for admin in admins %} - - - - -{% endfor %} -
        - {{ admin.user | pictureForAvatar('s') }} - - {{ admin.user.fullName }} - {% if admin.title is not empty %} -
        {{ admin.title }}
        - {% endif %} -
        -{% endblock %} diff --git a/src/main/resources/templates/common/left_menu.twig b/src/main/resources/templates/common/left_menu.twig index b68914e2..014330d1 100644 --- a/src/main/resources/templates/common/left_menu.twig +++ b/src/main/resources/templates/common/left_menu.twig @@ -20,7 +20,7 @@

        {{ L('reminder') }}

        {{ L('birthday_reminder_before', {'count': birthdayUsers | length}) -}} - {%- if L('birthday_reminder_day_pos').toString()!="end" %}{{ L(birthdaysAreToday ? 'birthday_reminder_today' : 'birthday_reminder_tomorrow') }}{% endif -%} + {%- if L('birthday_reminder_day_pos').toString()!="end" %}{{ L(birthdaysAreToday ? 'birthday_reminder_today' : 'birthday_reminder_tomorrow') }}{% endif -%} {{- L('birthday_reminder_middle', {'count': birthdayUsers | length}) -}} {%- for user in birthdayUsers -%} {{ L('birthday_reminder_name', {'name': user.firstLastAndGender}) }} @@ -28,7 +28,7 @@ {{- L(loop.revindex>1 ? 'birthday_reminder_separator' : 'birthday_reminder_separator_last') -}} {%- endif -%} {%- endfor -%} - {%- if L('birthday_reminder_day_pos').toString()=="end" %}{{ L('birthday_reminder_before_day', {'count': birthdayUsers | length}) }}{{ L(birthdaysAreToday ? 'birthday_reminder_today' : 'birthday_reminder_tomorrow') }}{% endif -%} + {%- if L('birthday_reminder_day_pos').toString()=="end" %}{{ L('birthday_reminder_before_day', {'count': birthdayUsers | length}) }}{{ L(birthdaysAreToday ? 'birthday_reminder_today' : 'birthday_reminder_tomorrow') }}{% endif -%} {{- L('birthday_reminder_after', {'count': birthdayUsers | length}) -}}
        {% endif %} @@ -36,7 +36,7 @@

        {{ L('reminder') }}

        {{ L('event_reminder_before', {'count': eventReminderEvents | length}) -}} - {%- if L('event_reminder_day_pos').toString()!="end" %}{{ L(eventsAreToday ? 'event_reminder_today' : 'event_reminder_tomorrow') }}{% endif -%} + {%- if L('event_reminder_day_pos').toString()!="end" %}{{ L(eventsAreToday ? 'event_reminder_today' : 'event_reminder_tomorrow') }}{% endif -%} {{- L('event_reminder_middle', {'count': eventReminderEvents | length}) -}} {%- for event in eventReminderEvents -%} {{ event.name }} @@ -44,7 +44,7 @@ {{- L(loop.revindex>1 ? 'event_reminder_separator' : 'event_reminder_separator_last') -}} {%- endif -%} {%- endfor -%} - {%- if L('event_reminder_day_pos').toString()=="end" %}{{ L('event_reminder_before_day', {'count': eventReminderEvents | length}) }}{{ L(eventsAreToday ? 'event_reminder_today' : 'event_reminder_tomorrow') }}{% endif -%} + {%- if L('event_reminder_day_pos').toString()=="end" %}{{ L('event_reminder_before_day', {'count': eventReminderEvents | length}) }}{{ L(eventsAreToday ? 'event_reminder_today' : 'event_reminder_tomorrow') }}{% endif -%} {{- L('event_reminder_after', {'count': eventReminderEvents | length}) -}}
        {% endif %} \ No newline at end of file diff --git a/src/main/resources/templates/desktop/events_actual_calendar.twig b/src/main/resources/templates/desktop/events_actual_calendar.twig new file mode 100644 index 00000000..28eab082 --- /dev/null +++ b/src/main/resources/templates/desktop/events_actual_calendar.twig @@ -0,0 +1,35 @@ +
        +
        + {{ L('events_calendar_title') }} +
        + + +
        + +
        + {{ L('date_format_month_year', {'month': L('month_standalone', {'month': month}), 'year': year}) }} +
        +
        +
        + {% for i in 1..7 %}
        {{ L('weekday_short', {'day': i}) }}
        {% endfor %} +
        +
        + {% for i in 1..(monthStartWeekday-1) %}
        {% endfor %} + {% for day in 1..monthLength %} +
        +
        {% if year==todayYear and month==todayMonth and day==todayDay %}{{ L('date_today') | capitalize }}{% else %}{{ day }}{% endif %}
        + {%- if calendarEvents[day] is not empty -%} +
        + {%- for actor in calendarEvents[day] | slice(0, min(calendarEvents[day] | length, 4)) -%} + {{ actor | pictureForAvatar('s', -1, wrapperClasses="ava#{loop.index}") }} + {%- endfor -%} +
        + {%- if (calendarEvents[day] | length)>4 -%} + {{ L('show_more') }} + {%- endif -%} + {%- endif -%} +
        + {% endfor %} + {% for i in 1..(6-((monthLength+monthStartWeekday-2)%7)) %}
        {% endfor %} +
        +
        \ No newline at end of file diff --git a/src/main/resources/templates/desktop/events_calendar.twig b/src/main/resources/templates/desktop/events_calendar.twig new file mode 100644 index 00000000..7943481a --- /dev/null +++ b/src/main/resources/templates/desktop/events_calendar.twig @@ -0,0 +1,57 @@ +{% extends "page" %} +{% block content %} +{% include "events_tabbar" with {'tab': 'calendar'} %} +
        {{ L('events_calendar_summary') }}
        +
        +{% for friend in birthdays %} + + + + + + +
        + {{friend | pictureForAvatar('m')}} + +
        +
        {{ L('name') }}:
        + +
        {{ L('birthday') }}:
        +
        {{ userDays[friend.id] }}
        +
        {{ L('birthday_turns') }}:
        +
        {{ userAges[friend.id] }}
        +
        +
        +
          +
        +
        +{% endfor %} +{% for group in events %} + + + + + + +
        + {{group | pictureForAvatar('m')}} + +
        +
        {{ L('group_name') }}:
        + +
        {{ L('group_size') }}:
        +
        {{ L('X_members', {'count': group.memberCount}) }}
        +
        {{ L('event_start_time') }}:
        +
        {{ LD(group.eventStartTime) }}
        +
        +
        +
          +
        +
        +{% endfor %} +
        + {% include "events_actual_calendar" %} +
        +
        +{% endblock %} + diff --git a/src/main/resources/templates/desktop/friend_requests.twig b/src/main/resources/templates/desktop/friend_requests.twig index 1f32279a..98cae466 100644 --- a/src/main/resources/templates/desktop/friend_requests.twig +++ b/src/main/resources/templates/desktop/friend_requests.twig @@ -14,50 +14,41 @@ {{req.from | pictureForAvatar('m')}} - - - - - +
        +
        {{ L('name') }}:
        + {% if req.from.domain is not empty %} -
        - - - +
        {{ L('server') }}:
        +
        {{ req.from.domain }}
        {% endif %} {% if req.message is not empty %} - - - - +
        {{ L('message') }}:
        +
        {{ req.message }}
        {% endif %} - - - - -
        {{ L('name') }}:{{ req.from.completeName }}
        {{ L('server') }}:{{ req.from.domain }}
        {{ L('message') }}:{{ req.message }}
        - {% if req.mutualFriendsCount>0 %} -
        - - {{- L('you_and_X_mutual_before', {'name': req.from.firstAndGender}) -}} - {{ L('you_and_X_mutual_count', {'numFriends': req.mutualFriendsCount}) }} - {{- L('you_and_X_mutual_after', {'name': req.from.firstAndGender}) -}} - -
        +
        + {% if req.mutualFriendsCount>0 %} +
        + + {{- L('you_and_X_mutual_before', {'name': req.from.firstAndGender}) -}} + {{ L('you_and_X_mutual_count', {'numFriends': req.mutualFriendsCount}) }} + {{- L('you_and_X_mutual_after', {'name': req.from.firstAndGender}) -}} + +
        {% for friend in req.mutualFriends %} {{ friend | pictureForAvatar('s', 32) }} {% endfor %} -
        - {% endif %} - - -
        - - -
        - -
        +
    + {% endif %} +
    + +
    + + +
    +
    +
    +
    diff --git a/src/main/resources/templates/desktop/friends.twig b/src/main/resources/templates/desktop/friends.twig index eca1c2fe..8f490c07 100644 --- a/src/main/resources/templates/desktop/friends.twig +++ b/src/main/resources/templates/desktop/friends.twig @@ -39,18 +39,14 @@ {{friend | pictureForAvatar('m')}} - - - - - +
    +
    {{ L('name') }}:
    + {% if friend.domain is not empty %} -
    - - - +
    {{ L('server') }}:
    +
    {{ friend.domain }}
    {% endif %} -
    {{ L('name') }}:{{ friend.completeName }}
    {{ L('server') }}:{{ friend.domain }}
    +