🔀 Merge “next” into “feature/notification-handlers”
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Thu, 2 Jan 2020 21:00:17 +0000 (22:00 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Thu, 2 Jan 2020 21:00:17 +0000 (22:00 +0100)
src/main/java/net/pterodactylus/sone/template/PostAccessor.java [deleted file]
src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt [new file with mode: 0644]
src/main/resources/i18n/sone.fr.properties
src/main/resources/templates/include/viewPost.html
src/test/java/net/pterodactylus/sone/template/PostAccessorTest.java [deleted file]
src/test/kotlin/net/pterodactylus/sone/template/PostAccessorTest.kt [new file with mode: 0644]

diff --git a/src/main/java/net/pterodactylus/sone/template/PostAccessor.java b/src/main/java/net/pterodactylus/sone/template/PostAccessor.java
deleted file mode 100644 (file)
index 4025c7b..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Sone - PostAccessor.java - Copyright Â© 2010–2019 David Roden
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package net.pterodactylus.sone.template;
-
-import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.Reply;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.util.template.ReflectionAccessor;
-import net.pterodactylus.util.template.TemplateContext;
-
-import com.google.common.collect.Collections2;
-
-/**
- * Accessor for {@link Post} objects that adds additional properties:
- * <dl>
- * <dd>replies</dd>
- * <dt>All replies to this post, sorted by time, oldest first</dt>
- * </dl>
- */
-public class PostAccessor extends ReflectionAccessor {
-
-       /** The core to get the replies from. */
-       private final Core core;
-
-       /**
-        * Creates a new post accessor.
-        *
-        * @param core
-        *            The core to get the replies from
-        */
-       public PostAccessor(Core core) {
-               this.core = core;
-       }
-
-       /**
-        * {@inheritDoc}
-        */
-       @Override
-       public Object get(TemplateContext templateContext, Object object, String member) {
-               Post post = (Post) object;
-               if ("replies".equals(member)) {
-                       return Collections2.filter(core.getReplies(post.getId()), Reply.FUTURE_REPLY_FILTER);
-               } else if (member.equals("likes")) {
-                       return core.getLikes(post);
-               } else if (member.equals("liked")) {
-                       Sone currentSone = (Sone) templateContext.get("currentSone");
-                       return (currentSone != null) && (currentSone.isLikedPostId(post.getId()));
-               } else if (member.equals("new")) {
-                       return !post.isKnown();
-               } else if (member.equals("bookmarked")) {
-                       return core.isBookmarked(post);
-               }
-               return super.get(templateContext, object, member);
-       }
-
-}
diff --git a/src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt b/src/main/kotlin/net/pterodactylus/sone/template/PostAccessor.kt
new file mode 100644 (file)
index 0000000..5222b94
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Sone - PostAccessor.java - Copyright Â© 2010–2019 David Roden
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.util.template.*
+
+/**
+ * Accessor for [Post] objects that adds additional properties:
+ *
+ * * `replies`: All replies to this post, sorted by time, oldest first
+ * * `likes`: All Sones that have liked the post
+ * * `liked`: `true` if the current Sone from the [template context][TemplateContext] has liked the post
+ * * `new`: `true` if the post is not known
+ * * `bookmarked`: `true` if the post is bookmarked
+ */
+class PostAccessor(private val core: Core) : ReflectionAccessor() {
+
+       override fun get(templateContext: TemplateContext?, `object`: Any?, member: String): Any? =
+                       (`object` as Post).let { post ->
+                               when (member) {
+                                       "replies" -> core.getReplies(post)
+                                       "likes" -> core.getLikes(post)
+                                       "liked" -> templateContext.currentSone?.isLikedPostId(post.id) ?: false
+                                       "new" -> !post.isKnown
+                                       "bookmarked" -> core.isBookmarked(post)
+                                       "replySone" -> core.getReplies(post)
+                                                       .lastOrNull { it.sone.isLocal }
+                                                       ?.sone
+                                                       ?: post.sone.takeIf { it.isLocal }
+                                                       ?: templateContext.currentSone
+                                       else -> super.get(templateContext, `object`, member)
+                               }
+                       }
+
+}
+
+private fun Core.getReplies(post: Post) = getReplies(post.id).filter { Reply.FUTURE_REPLY_FILTER.apply(it) }
+private val TemplateContext?.currentSone: Sone? get() = this?.get("currentSone") as? Sone
index b426bbc..24858aa 100644 (file)
@@ -52,14 +52,14 @@ Page.Options.Option.ShowAvatars.Followed.Description=Ne montrer que les avatars
 Page.Options.Option.ShowAvatars.ManuallyTrusted.Description=Ne montrer que les avatars des Sones auxquelles vous ĂȘtes assignĂ©s manuellement une confiance superieure Ă  0.
 Page.Options.Option.ShowAvatars.Trusted.Description=Ne montrer que les avatars des Sones qui ont une confiance superieure Ă  0.
 Page.Options.Option.ShowAvatars.Always.Description=Montre tout le temps les avatars personnalisĂ©s. Attention: certains avatars peuvent ĂȘtre offensants !
-Page.Options.Section.LoadLinkedImagesOptions.Title=Load Linked Images
-Page.Options.Option.LoadLinkedImages.Description=Sone can automatically try to load images that are linked to in posts and replies. This will only ever load images from Freenet, never from the internet!
-Page.Options.Option.LoadLinkedImages.Never.Description=Never load linked images.
-Page.Options.Option.LoadLinkedImages.Followed.Description=Only load images linked from Sones that you follow.
-Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description=Only load images linked from Sones that you have manually assigned a trust value larger than 0 to.
-Page.Options.Option.LoadLinkedImages.Trusted.Description=Only load images linked from Sones that have a trust value larger than 0.
-Page.Options.Option.LoadLinkedImages.Always.Description=Always load linked images. Be warned: some images might be disturbing or considered offensive.
-Page.Options.Section.RuntimeOptions.Title=Comportement runtime
+Page.Options.Section.LoadLinkedImagesOptions.Title=Chargement des images liĂ©es
+Page.Options.Option.LoadLinkedImages.Description=Sone essaie automatiquement de charger les images liĂ©es Ă  des messages ou Ă  des rĂ©ponses. Tous les chargements sont faits depuis Freenet, jamais depuis internet !
+Page.Options.Option.LoadLinkedImages.Never.Description=Ne jamais charger les images liĂ©es.
+Page.Options.Option.LoadLinkedImages.Followed.Description=Charge uniquement les images liĂ©es Ă  Sones que vous suivez.
+Page.Options.Option.LoadLinkedImages.ManuallyTrusted.Description=Ne charger que des images liĂ©es des Sones auxquelles vous ĂȘtes assignĂ©s manuellement une confiance superieure Ă  0.
+Page.Options.Option.LoadLinkedImages.Trusted.Description=Ne charger que des images liĂ©es Ă  des Sones qui ont une confiance superieure Ă  0.
+Page.Options.Option.LoadLinkedImages.Always.Description=Charge tout le temps les images liĂ©es. Attention: certains avatars peuvent ĂȘtre offensants !
+Page.Options.Section.RuntimeOptions.Title=Comportement de l'exĂ©cution
 Page.Options.Option.InsertionDelay.Description=Le dĂ©lai d'insertion d'un Sone.
 Page.Options.Option.PostsPerPage.Description=Le nombre de message Ă  afficher par page avant que les boutons de pagination soit affichĂ©s.
 Page.Options.Option.ImagesPerPage.Description=Le nombre de message Ă  afficher par page avant que les boutons de pagination soit affichĂ©s.
@@ -192,7 +192,7 @@ Page.ViewSone.Profile.Title=Profile
 Page.ViewSone.Profile.Label.Name=Nom
 Page.ViewSone.Profile.Label.Albums=Albums
 Page.ViewSone.Profile.Albums.Text.All=Tous les albums
-Page.ViewSone.Profile.Name.WoTLink=Profile Web of trust
+Page.ViewSone.Profile.Name.WoTLink=Profil Web of Trust
 Page.ViewSone.Replies.Title=Messages {sone} a rĂ©pondu Ă 
 
 Page.ViewPost.Title=Voir message - Sone
@@ -321,11 +321,11 @@ Page.Invalid.Title=Action invalide rĂ©alisĂ©e - Sone
 Page.Invalid.Page.Title=Action invalide rĂ©alisĂ©e
 Page.Invalid.Text=Une action invalide a Ă©tĂ© effectuĂ©e, ou l'action Ă©tait valide mes les paramĂštres ne l'Ă©taient pas. Veuillez retourner Ă  la {link}page d'index{/link} et veuillez rĂ©essayer. Si l'erreur persiste, vous avez probablement trouvĂ© un bug.
 
-Page.Metrics.Title=Metrics
-Page.Metrics.Page.Title=Metrics
-Page.Metrics.SoneInsertDuration.Title=Sone Insert Duration
-Page.Metrics.SoneParseDuration.Title=Sone Parse Duration
-Page.Metrics.ConfigurationSaveDuration.Title=Configuration Save Duration
+Page.Metrics.Title=MĂ©triques
+Page.Metrics.Page.Title=MĂ©triques
+Page.Metrics.SoneInsertDuration.Title=DurĂ©e d'insertion de Sone
+Page.Metrics.SoneParseDuration.Title=DurĂ©e d'analyse de Sone
+Page.Metrics.ConfigurationSaveDuration.Title=DurĂ©e de sauvegarde de configuration
 
 View.Search.Button.Search=Recherche
 
@@ -357,7 +357,7 @@ View.SoneMenu.Link.AllAlbums=Tous les Albums
 View.SoneMenu.WebOfTrustLink=Profile web of trust
 
 View.Post.UnknownAuthor=(inconnu)
-View.Post.WebOfTrustLink=Profile web of trust
+View.Post.WebOfTrustLink=Profil Web of Trust
 View.Post.Permalink=Lier le message
 View.Post.PermalinkAuthor=Lier l'auteur
 View.Post.Bookmarks.PostIsBookmarked=Ce message a Ă©tĂ© ajoutĂ© aux marque-pages, cliquer pour retirer des marque-pages
@@ -461,5 +461,5 @@ Notification.Mention.Text=Vous avez Ă©tĂ© mentionnĂ© dans les messages suivants:
 Notification.SoneIsInserting.Text=Votre Sone sone://{0} va maintenant ĂȘtre insĂ©rĂ©.
 Notification.SoneIsInserted.Text=votre Sone sone://{0} a Ă©tĂ© insĂ©rĂ© dans {1,number} {1,choice,0#seconds|1#second|1<seconds}.
 Notification.SoneInsertAborted.Text=Votre Sone sone://{0} ne peut pas ĂȘtre insĂ©rĂ©.
-Notification.SoneLockedOnStartup.Text=Versions prior to v81 had a bug that resulted in empty Sones. To prevent buggy Sones from being inserted they have been automatically locked. Please check your Sones, use the Rescue Mode if necessary, and unlock your Sones once you are satisfied with the results. Locked Sones are:
-# 55-61, 324–328, 360, 464
+Notification.SoneLockedOnStartup.Text=Les versions antĂ©rieures Ă  v81 avaient un bug vidant les Sones. Pour Ă©viter d'insĂ©rer des Sones vides ils ont Ă©tĂ© automatiquement vĂ©rouillĂ©s. VĂ©rifiez vos Sones, utilisez le mode rĂ©cupĂ©ration si nĂ©cessaire puis dĂ©vĂ©rouillez vos Sones lorsque vous ĂȘtes satisfait du rĂ©sultat. Les Sones vĂ©rouillĂ©s sont:
+# 464
index be11195..edcabce 100644 (file)
                                                <div class="sender">
                                                        <select name="sender" title="<%= View.UpdateStatus.Text.ChooseSenderIdentity|l10n|html>">
                                                                <%foreach localSones localSone|sort>
-                                                                       <option value="<% localSone.id|html>"<%if localSone.current> selected="selected"<%/if>><% localSone.niceName|html></option>
+                                                                       <option value="<% localSone.id|html>"<%if localSone|match value=post.replySone> selected="selected"<%/if>><% localSone.niceName|html></option>
                                                                <%/foreach>
                                                        </select>
                                                </div>
diff --git a/src/test/java/net/pterodactylus/sone/template/PostAccessorTest.java b/src/test/java/net/pterodactylus/sone/template/PostAccessorTest.java
deleted file mode 100644 (file)
index e60dce9..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-package net.pterodactylus.sone.template;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.contains;
-import static org.hamcrest.Matchers.is;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Set;
-
-import net.pterodactylus.sone.core.Core;
-import net.pterodactylus.sone.data.Post;
-import net.pterodactylus.sone.data.PostReply;
-import net.pterodactylus.sone.data.Sone;
-import net.pterodactylus.util.template.TemplateContext;
-
-import org.junit.Before;
-import org.junit.Test;
-
-/**
- * Unit test for {@link PostAccessor}.
- */
-public class PostAccessorTest {
-
-       private final Core core = mock(Core.class);
-       private final PostAccessor accessor = new PostAccessor(core);
-       private final Post post = mock(Post.class);
-
-       private final long now = System.currentTimeMillis();
-
-       @Before
-       public void setupPost() {
-               when(post.getId()).thenReturn("post-id");
-       }
-
-       @Test
-       @SuppressWarnings("unchecked")
-       public void accessorReturnsTheCorrectReplies() {
-               List<PostReply> replies = new ArrayList<>();
-               replies.add(createPostReply(2000));
-               replies.add(createPostReply(-1000));
-               replies.add(createPostReply(-2000));
-               replies.add(createPostReply(-3000));
-               replies.add(createPostReply(-4000));
-               when(core.getReplies("post-id")).thenReturn(replies);
-               Collection<PostReply> repliesForPost = (Collection<PostReply>) accessor.get(null, post, "replies");
-               assertThat(repliesForPost, contains(
-                               replies.get(1),
-                               replies.get(2),
-                               replies.get(3),
-                               replies.get(4)
-               ));
-       }
-
-       private PostReply createPostReply(long timeOffset) {
-               PostReply postReply = mock(PostReply.class);
-               when(postReply.getTime()).thenReturn(now + timeOffset);
-               return postReply;
-       }
-
-       @Test
-       @SuppressWarnings("unchecked")
-       public void accessorReturnsTheLikingSones() {
-               Set<Sone> sones = mock(Set.class);
-               when(core.getLikes(post)).thenReturn(sones);
-               Set<Sone> likingSones = (Set<Sone>) accessor.get(null, post, "likes");
-               assertThat(likingSones, is(sones));
-       }
-
-       @Test
-       public void accessorReturnsWhetherTheCurrentSoneLikedAPost() {
-               Sone sone = mock(Sone.class);
-               when(sone.isLikedPostId("post-id")).thenReturn(true);
-               TemplateContext templateContext = new TemplateContext();
-               templateContext.set("currentSone", sone);
-               assertThat(accessor.get(templateContext, post, "liked"), is((Object) true));
-       }
-
-       @Test
-       public void accessorReturnsFalseIfPostIsNotLiked() {
-               Sone sone = mock(Sone.class);
-               TemplateContext templateContext = new TemplateContext();
-               templateContext.set("currentSone", sone);
-               assertThat(accessor.get(templateContext, post, "liked"), is((Object) false));
-       }
-
-       @Test
-       public void accessorReturnsFalseIfThereIsNoCurrentSone() {
-               TemplateContext templateContext = new TemplateContext();
-               assertThat(accessor.get(templateContext, post, "liked"), is((Object) false));
-       }
-
-       @Test
-       public void accessorReturnsThatNotKnownPostIsNew() {
-               assertThat(accessor.get(null, post, "new"), is((Object) true));
-       }
-
-       @Test
-       public void accessorReturnsThatKnownPostIsNotNew() {
-               when(post.isKnown()).thenReturn(true);
-               assertThat(accessor.get(null, post, "new"), is((Object) false));
-       }
-
-       @Test
-       public void accessorReturnsIfPostIsBookmarked() {
-               when(core.isBookmarked(post)).thenReturn(true);
-               assertThat(accessor.get(null, post, "bookmarked"), is((Object) true));
-       }
-
-       @Test
-       public void accessorReturnsOtherProperties() {
-               assertThat(accessor.get(null, post, "hashCode"), is((Object) post.hashCode()));
-       }
-
-}
diff --git a/src/test/kotlin/net/pterodactylus/sone/template/PostAccessorTest.kt b/src/test/kotlin/net/pterodactylus/sone/template/PostAccessorTest.kt
new file mode 100644 (file)
index 0000000..72e1343
--- /dev/null
@@ -0,0 +1,187 @@
+package net.pterodactylus.sone.template
+
+import net.pterodactylus.sone.core.*
+import net.pterodactylus.sone.data.*
+import net.pterodactylus.sone.test.*
+import net.pterodactylus.util.template.*
+import org.hamcrest.MatcherAssert.*
+import org.hamcrest.Matchers.*
+import org.junit.*
+
+/**
+ * Unit test for [PostAccessor].
+ */
+class PostAccessorTest {
+
+       private val core = mock<Core>()
+       private val accessor = PostAccessor(core)
+       private val post = mock<Post>()
+       private val now = System.currentTimeMillis()
+
+       @Before
+       fun setupPost() {
+               whenever(post.id).thenReturn("post-id")
+       }
+
+       @Test
+       fun `accessor returns the correct replies`() {
+               val replies = listOf(
+                               createPostReply(2000),
+                               createPostReply(-1000),
+                               createPostReply(-2000),
+                               createPostReply(-3000),
+                               createPostReply(-4000)
+               )
+               whenever(core.getReplies("post-id")).thenReturn(replies)
+               val repliesForPost = accessor[null, post, "replies"] as Collection<PostReply>
+               assertThat(repliesForPost, contains(
+                               replies[1],
+                               replies[2],
+                               replies[3],
+                               replies[4]
+               ))
+       }
+
+       private fun createPostReply(timeOffset: Long) = mock<PostReply>().apply {
+               whenever(time).thenReturn(now + timeOffset)
+       }
+
+       @Test
+       fun `accessor returns the liking sones`() {
+               val sones = setOf<Sone>()
+               whenever(core.getLikes(post)).thenReturn(sones)
+               val likingSones = accessor[null, post, "likes"] as Set<Sone>
+               assertThat(likingSones, equalTo(sones))
+       }
+
+       @Test
+       fun `accessor returns whether the current sone liked a post`() {
+               val sone = mock<Sone>()
+               whenever(sone.isLikedPostId("post-id")).thenReturn(true)
+               val templateContext = TemplateContext()
+               templateContext["currentSone"] = sone
+               assertThat(accessor[templateContext, post, "liked"], equalTo<Any>(true))
+       }
+
+       @Test
+       fun `accessor returns false if post is not liked`() {
+               val sone = mock<Sone>()
+               val templateContext = TemplateContext()
+               templateContext["currentSone"] = sone
+               assertThat(accessor[templateContext, post, "liked"], equalTo<Any>(false))
+       }
+
+       @Test
+       fun `accessor returns false if there is no current sone`() {
+               val templateContext = TemplateContext()
+               assertThat(accessor[templateContext, post, "liked"], equalTo<Any>(false))
+       }
+
+       @Test
+       fun `accessor returns that not known post is new`() {
+               assertThat(accessor[null, post, "new"], equalTo<Any>(true))
+       }
+
+       @Test
+       fun `accessor returns that known post is not new`() {
+               whenever(post.isKnown).thenReturn(true)
+               assertThat(accessor[null, post, "new"], equalTo<Any>(false))
+       }
+
+       @Test
+       fun `accessor returns if post is bookmarked`() {
+               whenever(core.isBookmarked(post)).thenReturn(true)
+               assertThat(accessor[null, post, "bookmarked"], equalTo<Any>(true))
+       }
+
+       @Test
+       fun `reply sone for remote post without replies is current sone`() {
+               val post = mockPostFrom(remoteSone)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(currentSone))
+       }
+
+       @Test
+       fun `reply sone for remote post with remote replies is current sone`() {
+               val post = mockPostFrom(remoteSone)
+               val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(remoteSone))
+               whenever(core.getReplies("post-id")).thenReturn(replies)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(currentSone))
+       }
+
+       @Test
+       fun `reply sone for remote post with remote and one local replies is sone of local reply`() {
+               val post = mockPostFrom(remoteSone)
+               val localSone = mockLocalSone()
+               val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(localSone))
+               whenever(core.getReplies("post-id")).thenReturn(replies)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(localSone))
+       }
+
+       @Test
+       fun `reply sone for remote post with remote and several local replies is sone of latest local reply`() {
+               val post = mockPostFrom(remoteSone)
+               val localSone1 = mockLocalSone()
+               val localSone2 = mockLocalSone()
+               val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(localSone1), mockReplyFrom(localSone2))
+               whenever(core.getReplies("post-id")).thenReturn(replies)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(localSone2))
+       }
+
+       @Test
+       fun `reply sone for local post without replies is post sone`() {
+               val localSone = mockLocalSone()
+               val post = mockPostFrom(localSone)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(localSone))
+       }
+
+       @Test
+       fun `reply sone for local post with remote replies is local sone`() {
+               val localSone = mockLocalSone()
+               val post = mockPostFrom(localSone)
+               val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(remoteSone))
+               whenever(core.getReplies("post-id")).thenReturn(replies)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(localSone))
+       }
+
+       @Test
+       fun `reply sone for local post with remote and one local replies is local reply sone`() {
+               val localSone1 = mockLocalSone()
+               val post = mockPostFrom(localSone1)
+               val localSone2 = mockLocalSone()
+               val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(localSone2))
+               whenever(core.getReplies("post-id")).thenReturn(replies)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(localSone2))
+       }
+
+       @Test
+       fun `reply sone for local post with remote and several local replies is latest local reply sone`() {
+               val localSone1 = mockLocalSone()
+               val post = mockPostFrom(localSone1)
+               val localSone2 = mockLocalSone()
+               val localSone3 = mockLocalSone()
+               val replies = listOf(mockReplyFrom(remoteSone), mockReplyFrom(localSone2), mockReplyFrom(localSone3))
+               whenever(core.getReplies("post-id")).thenReturn(replies)
+               assertThat(accessor[templateContext, post, "replySone"], equalTo<Any>(localSone3))
+       }
+
+       @Test
+       fun `accessor returns other properties`() {
+               assertThat(accessor[null, post, "hashCode"], equalTo<Any>(post.hashCode()))
+       }
+
+}
+
+private val currentSone = mock<Sone>()
+private val remoteSone = mock<Sone>()
+private fun mockLocalSone() = mock<Sone>().apply { whenever(isLocal).thenReturn(true) }
+
+private val templateContext = TemplateContext().apply {
+       this["currentSone"] = currentSone
+}
+
+private fun mockPostFrom(sone: Sone) = mock<Post>().apply {
+       whenever(id).thenReturn("post-id")
+       whenever(this.sone).thenReturn(sone)
+}
+
+private fun mockReplyFrom(sone: Sone) = mock<PostReply>().apply { whenever(this.sone).thenReturn(sone) }