update fork #128

Manually merged
tristan merged 181 commits from mirror/monocles_chat_clean:master into master 2026-01-23 14:02:38 +01:00
11 changed files with 209 additions and 111 deletions
Showing only changes of commit 56a17c1e07 - Show all commits

Add like button to posts
Some checks are pending
Android CI / build (pull_request) Waiting to run

Arne 2026-01-20 13:34:03 +01:00

View file

@ -927,7 +927,7 @@ public class IqGenerator extends AbstractGenerator {
return iq;
}
public Iq publishComment(final Account account, final String node, final String title, final String inReplyToId) {
public Iq publishComment(final Account account, final String node, final String title) {
final Iq iq = new Iq(Iq.Type.SET);
iq.setTo(account.getJid().asBareJid());
final Element pubsub = iq.addChild("pubsub", Namespace.PUBSUB);

View file

@ -8225,7 +8225,7 @@ public class XmppConnectionService extends Service {
}
}
public void publishComment(final Account account, final String nodeUri, final String title, final String inReplyToId, final OnPostPublished callback) {
public void publishComment(final Account account, final String nodeUri, final String title, final OnPostPublished callback) {
if (account == null) {
if (callback != null) {
callback.onPostPublishFailed();
@ -8263,7 +8263,7 @@ public class XmppConnectionService extends Service {
Log.d(Config.LOGTAG, "could not create comments node " + response);
}
}
final Iq publishRequest = getIqGenerator().publishComment(account, targetNode, title, inReplyToId);
final Iq publishRequest = getIqGenerator().publishComment(account, targetNode, title);
publishRequest.setTo(to);
sendIqPacket(account, publishRequest, publishResponse -> {
if (publishResponse.getType() == Iq.Type.RESULT) {

View file

@ -238,7 +238,7 @@ public class CreatePostActivity extends XmppActivity {
}
if (inReplyToNode != null) {
xmppConnectionService.publishComment(selectedAccount, inReplyToNode, content, inReplyToId, new XmppConnectionService.OnPostPublished() {
xmppConnectionService.publishComment(selectedAccount, inReplyToNode, content, new XmppConnectionService.OnPostPublished() {
@Override
public void onPostPublished() {
runOnUiThread(() -> {

View file

@ -162,23 +162,44 @@ public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.PostViewHold
}
void bind(Post post) {
// Always reset the state of recycled views to a clean, default state before binding
binding.commentsList.setVisibility(View.GONE);
binding.commentsList.setAdapter(null);
binding.likeButton.setOnClickListener(null);
binding.likeCount.setText("");
binding.likeButton.setEnabled(false); // Disable until content is loaded
binding.likeButton.setCompoundDrawablesWithIntrinsicBounds(R.drawable.outline_favorite_border_24, 0, 0, 0);
final boolean isExpanded = expandedPosts.contains(post);
// Set visibility of summary/full content based on expanded state
binding.postContentSummary.setVisibility(isExpanded ? View.GONE : View.VISIBLE);
binding.postContentFull.setVisibility(isExpanded ? View.VISIBLE : View.GONE);
binding.postActions.setVisibility(isExpanded ? View.VISIBLE : View.GONE);
if (isExpanded && post.getCommentsNode() != null) {
loadComments(post, binding.commentsList);
} else {
binding.commentsList.setVisibility(View.GONE);
binding.commentsList.setAdapter(null);
if (isExpanded) {
// Asynchronously load all dynamic content (comments and likes)
loadCommentsAndLikes(post, this);
}
final View.OnClickListener expandClickListener = v -> {
if (expandedPosts.contains(post)) {
expandedPosts.remove(post);
} else {
expandedPosts.add(post);}
notifyItemChanged(getAdapterPosition());
};
// This is the full, non-omitted logic to bind the main post view
setupPostView(post, expandClickListener);
}
private void setupPostView(final Post post, final View.OnClickListener expandClickListener) {
final boolean isExpanded = expandedPosts.contains(post);
final boolean hasAttachment = post.getAttachmentUrl() != null;
final boolean isImage = hasAttachment && post.getAttachmentType() != null && post.getAttachmentType().startsWith("image/");
final boolean isVideo = hasAttachment && post.getAttachmentType() != null && post.getAttachmentType().startsWith("video/");
binding.postContentSummary.setVisibility(isExpanded ? View.GONE : View.VISIBLE);
binding.postContentFull.setVisibility(isExpanded ? View.VISIBLE : View.GONE);
binding.postActions.setVisibility(isExpanded ? View.VISIBLE : View.GONE);
binding.attachmentHint.setVisibility(hasAttachment && !isExpanded ? View.VISIBLE : View.GONE);
binding.postImage.setVisibility(isExpanded && (isImage || isVideo) ? View.VISIBLE : View.GONE);
binding.videoOverlayIcon.setVisibility(isExpanded && isVideo ? View.VISIBLE : View.GONE);
@ -186,43 +207,24 @@ public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.PostViewHold
if (isExpanded && (isImage || isVideo)) {
binding.attachmentProgress.setVisibility(View.VISIBLE);
if (isImage) {
Glide.with(mActivity)
.load(post.getAttachmentUrl())
.listener(new com.bumptech.glide.request.RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable com.bumptech.glide.load.engine.GlideException e, @Nullable Object model, @NonNull com.bumptech.glide.request.target.Target<Drawable> target, boolean isFirstResource) {
binding.attachmentProgress.setVisibility(View.GONE);
return false;
}
Glide.with(mActivity)
.load(post.getAttachmentUrl()) .listener(new com.bumptech.glide.request.RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable com.bumptech.glide.load.engine.GlideException e, @Nullable Object model, @NonNull com.bumptech.glide.request.target.Target<Drawable> target, boolean isFirstResource) {
binding.attachmentProgress.setVisibility(View.GONE);
return false;
}
@Override
public boolean onResourceReady(@NonNull Drawable resource, @NonNull Object model, @NonNull com.bumptech.glide.request.target.Target<Drawable> target, @NonNull com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) {
binding.attachmentProgress.setVisibility(View.GONE);
return false;
}
})
.into(binding.postImage);
binding.postImage.setOnClickListener(v -> showImagePreviewDialog(post.getAttachmentUrl()));
} else {
Glide.with(mActivity)
.load(post.getAttachmentUrl())
.listener(new com.bumptech.glide.request.RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable com.bumptech.glide.load.engine.GlideException e, @Nullable Object model, @NonNull com.bumptech.glide.request.target.Target<Drawable> target, boolean isFirstResource) {
binding.attachmentProgress.setVisibility(View.GONE);
return false;
}
@Override
public boolean onResourceReady(@NonNull Drawable resource, @NonNull Object model, @NonNull com.bumptech.glide.request.target.Target<Drawable> target, @NonNull com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) {
binding.attachmentProgress.setVisibility(View.GONE);
return false;
}
})
.into(binding.postImage);
binding.postImage.setOnClickListener(v -> showVideoPreviewDialog(post.getAttachmentUrl()));
}
@Override
public boolean onResourceReady(@NonNull Drawable resource, @NonNull Object model, @NonNull com.bumptech.glide.request.target.Target<Drawable> target, @NonNull com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) {
binding.attachmentProgress.setVisibility(View.GONE);
return false;
}
})
.into(binding.postImage);binding.postImage.setOnClickListener(v -> {
if (isImage) showImagePreviewDialog(post.getAttachmentUrl());
else showVideoPreviewDialog(post.getAttachmentUrl());
});
}
final List<Account> postAccounts = new ArrayList<>();
@ -231,8 +233,7 @@ public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.PostViewHold
if (account.isOnlineAndConnected()) {
if (account.getJid().asBareJid().equals(post.getAuthor().asBareJid()) || (account.getRoster() != null && account.getRoster().getContact(post.getAuthor().asBareJid()) != null)) {
postAccounts.add(account);
}
}
} }
}
}
if (mActivity.xmppConnectionService == null) {
@ -241,58 +242,23 @@ public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.PostViewHold
binding.replyButton.setVisibility(View.GONE);
binding.commentButton.setVisibility(View.GONE);
binding.downloadButton.setVisibility(View.GONE);
binding.likeButton.setVisibility(View.GONE);
} else {
final Account ownAccount = post.getAuthor() != null ? mActivity.xmppConnectionService.findAccountByJid(post.getAuthor().asBareJid()) : null;
binding.downloadButton.setOnClickListener(v -> {
if (postAccounts.size() == 1) {
downloadAttachment(postAccounts.get(0), post);
} else {
showAccountSelectionDialog(mActivity.getString(R.string.choose_account_for_download), postAccounts, account -> downloadAttachment(account, post));
} else { showAccountSelectionDialog(mActivity.getString(R.string.choose_account_for_download), postAccounts, account -> downloadAttachment(account, post));
}
});
if (post.getContent() != null) {
final CharSequence markdown = markwon.toMarkdown(post.getContent());
final SpannableString spannable = new SpannableString(markdown);
final Matcher matcher = Pattern.compile("#[\\p{L}\\p{N}]+").matcher(spannable);
while (matcher.find()) {
final String hashtag = matcher.group(0);
spannable.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
mOnSearchPerformed.onSearchPerformed(hashtag);
}
}, matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
binding.postContentSummary.setText(spannable);
binding.postContentFull.setText(spannable);
binding.postContentSummary.setMovementMethod(android.text.method.LinkMovementMethod.getInstance());
binding.postContentFull.setMovementMethod(android.text.method.LinkMovementMethod.getInstance());
} else {binding.postContentSummary.setText("");
binding.postContentFull.setText("");
}
final View.OnClickListener expandClickListener = v -> {
if (expandedPosts.contains(post)) {
expandedPosts.remove(post);
} else {
expandedPosts.add(post);
}
notifyItemChanged(getAdapterPosition());
};
if (post.getContent() != null) {
final CharSequence markdown = markwon.toMarkdown(post.getContent());
// For the summary view, just set the text and an expand listener. No clickable spans.
binding.postContentSummary.setText(markdown);
binding.postContentSummary.setMovementMethod(null);
binding.postContentSummary.setOnClickListener(expandClickListener);
// For the full view, make hashtags clickable.
final SpannableString spannable = new SpannableString(markdown);
final Matcher matcher = Pattern.compile("#[\\p{L}\\p{N}]+").matcher(spannable);
while (matcher.find()) {
@ -304,9 +270,10 @@ public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.PostViewHold
}
}, matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
binding.postContentFull.setText(spannable);
binding.postContentFull.setMovementMethod(android.text.method.LinkMovementMethod.getInstance());
binding.postContentFull.setOnClickListener(null); // Clicks handled by spans, not the view itself
binding.postContentFull.setOnClickListener(null);
} else {
binding.postContentSummary.setText("");
@ -315,7 +282,6 @@ public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.PostViewHold
itemView.setOnClickListener(expandClickListener);
binding.postTitle.setOnClickListener(expandClickListener);
binding.postContentSummary.setOnClickListener(expandClickListener);
binding.replyButton.setOnClickListener(v -> {
if (post.getAuthor() != null) {
@ -332,7 +298,7 @@ public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.PostViewHold
binding.shareButton.setOnClickListener(v -> {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, post.getTitle() + "\\n" + post.getContent());
intent.putExtra(Intent.EXTRA_TEXT, post.getTitle() + "\n" + post.getContent());
mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.share_post_with)));
});
@ -430,9 +396,10 @@ public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.PostViewHold
}
}
private void loadComments(final Post post, final RecyclerView recyclerView) {if (mActivity.xmppConnectionService == null) {
return;
}
private void loadCommentsAndLikes(final Post post, final PostViewHolder holder) {
if (mActivity.xmppConnectionService == null) {
return;
}
try {
final XmppUri uri = new XmppUri(post.getCommentsNode());
final Jid jid = uri.getJid();
@ -446,6 +413,7 @@ public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.PostViewHold
reader.setInputStream(new java.io.ByteArrayInputStream(feedXml.getBytes()));
final Element pubsub = reader.readElement(reader.readTag());
final List<Comment> comments = new ArrayList<>();
final List<Comment> likes = new ArrayList<>();
if (pubsub != null) {
final Element items = pubsub.findChild("items");
if (items != null) {
@ -453,34 +421,39 @@ public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.PostViewHold
if ("item".equals(item.getName())) {
final Comment comment = Comment.fromElement(item);
if (comment != null) {
comments.add(comment);
if ("".equals(comment.getTitle())) {
likes.add(comment);
} else {
comments.add(comment);
}
}
}
}
}
}
mActivity.runOnUiThread(() -> {
if (holder.getAdapterPosition() == RecyclerView.NO_POSITION || posts.get(holder.getAdapterPosition()) != post) {
return; // Don't update a recycled view
}
// Update comments list UI
if (!comments.isEmpty()) {
java.util.Collections.sort(comments, (c1, c2) -> {
Date d1 = c1.getPublished();
Date d2 = c2.getPublished();
if (d1 == null && d2 == null) {
return 0;
} else if (d1 == null) {
return -1;
} else if (d2 == null) {
return 1;
} else {
return d1.compareTo(d2);
}
if (d1 == null && d2 == null) return 0;
if (d1 == null) return -1;
if (d2 == null) return 1;
return d1.compareTo(d2);
});
CommentsAdapter commentsAdapter = new CommentsAdapter(mActivity, post, comments);
recyclerView.setLayoutManager(new LinearLayoutManager(mActivity));
recyclerView.setAdapter(commentsAdapter);
recyclerView.setVisibility(View.VISIBLE);
holder.binding.commentsList.setLayoutManager(new LinearLayoutManager(mActivity));
holder.binding.commentsList.setAdapter(commentsAdapter);
holder.binding.commentsList.setVisibility(View.VISIBLE);
} else {
recyclerView.setVisibility(View.GONE);
holder.binding.commentsList.setVisibility(View.GONE);
}
// Update like button UI and logic
setupLikeButton(post, holder, likes);
});
} catch (Exception e) {
Log.e(Config.LOGTAG, "error parsing comments", e);
@ -497,9 +470,92 @@ public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.PostViewHold
Log.e(Config.LOGTAG, "error parsing comments node uri", e);
}
}
private void setupLikeButton(final Post post, final PostViewHolder holder, final List<Comment> likes) {
final List<Account> onlineAccounts = mActivity.xmppConnectionService.getAccounts().stream()
.filter(Account::isOnlineAndConnected)
.collect(java.util.stream.Collectors.toList());
if (onlineAccounts.isEmpty()) {
holder.binding.likeButton.setEnabled(false);
holder.binding.likeCount.setText(String.valueOf(likes.size()));
return;
}
holder.binding.likeButton.setEnabled(true);
holder.binding.likeCount.setText(String.valueOf(likes.size()));
Comment myLike = null;
Account myLikerAccount = null;
for (Account acc : onlineAccounts) {
for (Comment like : likes) {
if (like.getAuthor() != null && like.getAuthor().asBareJid().equals(acc.getJid().asBareJid())) {
myLike = like;
myLikerAccount = acc;
break;
}
}
if (myLike != null) break;
}
final boolean hasLiked = myLike != null;
holder.binding.likeButton.setCompoundDrawablesWithIntrinsicBounds(
hasLiked ? R.drawable.outline_favorite_24 : R.drawable.outline_favorite_border_24, 0, 0, 0);
final Comment finalMyLike = myLike;
final Account finalMyLikerAccount = myLikerAccount;
holder.binding.likeButton.setOnClickListener(v -> {
if (hasLiked && finalMyLikerAccount != null) {
// Unlike is simple: use the account that was found to have liked the post
retractLike(finalMyLikerAccount, finalMyLike, post, holder);
} else {
// Like: if there are multiple accounts, let the user choose
if (onlineAccounts.size() > 1) {
showAccountSelectionDialog(mActivity.getString(R.string.choose_account_for_like), onlineAccounts, selectedAccount -> {
publishLike(selectedAccount, post, holder);
});
} else {
publishLike(onlineAccounts.get(0), post, holder);
}
}
});
}
private void publishLike(final Account account, final Post post, final PostViewHolder holder) {
mActivity.xmppConnectionService.publishComment(account, post.getCommentsNode(), "", new XmppConnectionService.OnPostPublished() {
@Override
public void onPostPublished() {
mActivity.runOnUiThread(() -> loadCommentsAndLikes(post, holder));
}
@Override
public void onPostPublishFailed() {
mActivity.runOnUiThread(() -> Toast.makeText(mActivity, R.string.error_liking_post, Toast.LENGTH_SHORT).show());
}
});
}
private void retractLike(final Account account, final Comment like, final Post post, final PostViewHolder holder) {
try {
final XmppUri uri = new XmppUri(post.getCommentsNode());
final Jid jid = uri.getJid();
final String node = uri.getParameter("node");
mActivity.xmppConnectionService.retractPost(account, jid, node, like.getId(), new XmppConnectionService.OnPostRetracted() {
@Override
public void onPostRetracted(String postId) {
mActivity.runOnUiThread(() -> loadCommentsAndLikes(post, holder));
}
@Override
public void onPostRetractionFailed() {
mActivity.runOnUiThread(() -> Toast.makeText(mActivity, R.string.error_removing_like, Toast.LENGTH_SHORT).show());
}
});
} catch (Exception e) {
Log.e(Config.LOGTAG, "error retracting like", e);
mActivity.runOnUiThread(() -> Toast.makeText(mActivity, R.string.error_removing_like, Toast.LENGTH_SHORT).show());
}
}
}
private void downloadAttachment(Account account, Post post) {
private void downloadAttachment(Account account, Post post) {
if (mActivity.xmppConnectionService == null) {
return;
}

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="18dp" android:tint="?colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="18dp">
<path android:fillColor="@android:color/white" android:pathData="M280,840Q247,840 223.5,816.5Q200,793 200,760L200,240L160,240L160,160L360,160L360,120L600,120L600,160L800,160L800,240L760,240L760,760Q760,793 736.5,816.5Q713,840 680,840L280,840ZM680,240L280,240L280,760Q280,760 280,760Q280,760 280,760L680,760Q680,760 680,760Q680,760 680,760L680,240ZM360,680L440,680L440,320L360,320L360,680ZM520,680L600,680L600,320L520,320L520,680ZM280,240L280,240L280,760Q280,760 280,760Q280,760 280,760L280,760Q280,760 280,760Q280,760 280,760L280,240Z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459L440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459Q440,459 440,459ZM440,840L313,726Q241,661 189.5,610Q138,559 104.5,514Q71,469 55.5,427Q40,385 40,339Q40,245 103,182.5Q166,120 260,120Q312,120 359,142Q406,164 440,204Q474,164 521,142Q568,120 620,120Q701,120 756,165.5Q811,211 831,280Q831,280 817.5,280Q804,280 788.5,280Q773,280 759.5,280Q746,280 746,280Q728,240 693,220Q658,200 620,200Q569,200 532,227.5Q495,255 463,300L417,300Q386,255 346.5,227.5Q307,200 260,200Q203,200 161.5,239.5Q120,279 120,339Q120,372 134,406Q148,440 184,484.5Q220,529 282,588.5Q344,648 440,732Q466,709 501,679Q536,649 557,629Q557,629 566,638Q575,647 585.5,657.5Q596,668 605,677Q614,686 614,686Q592,706 558,735.5Q524,765 498,788L440,840ZM720,680L720,560L600,560L600,480L720,480L720,360L800,360L800,480L920,480L920,560L800,560L800,680L720,680Z"/>
</vector>

View file

@ -26,7 +26,7 @@
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/retract_comment"
android:src="@drawable/ic_delete_white_24dp"
android:src="@drawable/delete_18dp"
app:layout_constraintBottom_toBottomOf="@+id/comment_author_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/comment_author_name"

View file

@ -38,7 +38,7 @@
android:id="@+id/post_timestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android.layout_marginStart="8dp"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
@ -138,7 +138,7 @@
android:layout_marginTop="8dp"
android:text="@string/download_file"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/post_title"
app:layout_constraintTop_toBottomOf="@+id/post_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible" />
@ -182,6 +182,22 @@
app:layout_constraintTop_toBottomOf="@id/post_content_full"
tools:visibility="visible">
<Button
android:id="@+id/like_button"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="42dp"
android:layout_height="wrap_content"
app:icon="@drawable/outline_favorite_border_24" />
<TextView
android:id="@+id/like_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="-8dp"
android:textColor="?android:attr/textColorSecondary"
tools:text="5" />
<Button
android:id="@+id/reply_button"
style="@style/Widget.Material3.Button.TextButton"

View file

@ -1638,6 +1638,12 @@
<string name="retract_comment">Retract comment</string>
<string name="retract_comment_confirm">Do you really want to retract this comment?</string>
<string name="error_retracting_comment">Could not retract comment</string>
<string name="like">Like</string>
<string name="like_added">Like added</string>
<string name="error_liking_post">Could not like post</string>
<string name="like_removed">Like removed</string>
<string name="error_removing_like">Could not remove like</string>
<string name="choose_account_for_like">Like post with</string>
<plurals name="publishing_to_x_contacts">
<item quantity="one">Publishing to %d contact</item>
<item quantity="other">Publishing to %d contacts</item>