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
12 changed files with 134 additions and 39 deletions
Showing only changes of commit a7bcce88ff - Show all commits

Live handling of posts comments

Arne 2026-01-21 23:54:35 +01:00

View file

@ -34,6 +34,7 @@ public abstract class AbstractGenerator {
Namespace.OOB,
"http://jabber.org/protocol/caps",
"http://jabber.org/protocol/disco#info",
Namespace.PUBSUB,
"urn:xmpp:avatar:metadata+notify",
Namespace.NICK + "+notify",
"urn:xmpp:ping",
@ -41,6 +42,12 @@ public abstract class AbstractGenerator {
"http://jabber.org/protocol/chatstates",
Namespace.REACTIONS,
Namespace.USER_TUNE + "+notify",
Namespace.PUBSUB_SOCIAL_FEED,
Namespace.PUBSUB_SOCIAL_FEED + "+notify",
Namespace.MICROBLOG,
Namespace.MICROBLOG + "+notify",
Namespace.PUBSUB_STORIES,
Namespace.PUBSUB_STORIES + "+notify",
};
private final String[] MESSAGE_CONFIRMATION_FEATURES = {
"urn:xmpp:chat-markers:0", "urn:xmpp:receipts"
@ -116,12 +123,6 @@ public abstract class AbstractGenerator {
final ArrayList<String> features = new ArrayList<>(Arrays.asList(STATIC_FEATURES));
features.add("http://jabber.org/protocol/xhtml-im");
features.add("urn:xmpp:bob");
features.add(Namespace.PUBSUB_SOCIAL_FEED);
features.add(Namespace.PUBSUB_SOCIAL_FEED + "+notify");
features.add(Namespace.PUBSUB_MICROBLOG);
features.add(Namespace.PUBSUB_MICROBLOG + "+notify");
features.add(Namespace.PUBSUB_STORIES);
features.add(Namespace.PUBSUB_STORIES + "+notify");
if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION) {
features.add(Namespace.MDS_DISPLAYED + "+notify");
}

View file

@ -248,15 +248,12 @@ public class IqGenerator extends AbstractGenerator {
public Iq publishStory(final Account account, final String url, final String type, final String title, Bundle options) {
final Element item = new Element("item");
// This is the fix: Generate a single ID for both the pubsub item and the atom entry.
final String storyId = UUID.randomUUID().toString();
item.setAttribute("id", storyId);
final Element entry = item.addChild("entry", Namespace.ATOM);
// atom:id is a mandatory element for the entry, must be a unique and permanent URI
entry.addChild("id").setContent("urn:uuid:" + storyId);
// atom:title is mandatory
String effectiveTitle = title;
if (Strings.isNullOrEmpty(effectiveTitle)) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
@ -264,7 +261,6 @@ public class IqGenerator extends AbstractGenerator {
}
entry.addChild("title").setContent(effectiveTitle);
// atom:updated is mandatory
final String timestamp = getTimestamp(System.currentTimeMillis());
entry.addChild("updated").setContent(timestamp);
entry.addChild("published").setContent(timestamp);
@ -273,7 +269,6 @@ public class IqGenerator extends AbstractGenerator {
entry.addChild("author").addChild("uri").setContent("xmpp:" + account.getJid().asBareJid());
}
// The <link> element as specified by the XEP
final Element link = entry.addChild("link");
link.setAttribute("rel", "enclosure");
link.setAttribute("href", url);
@ -841,7 +836,7 @@ public class IqGenerator extends AbstractGenerator {
item.setAttribute("id", postId);
final Element entry = item.addChild("entry", Namespace.ATOM);
entry.addChild("id").setContent(postId);
entry.addChild("id").setContent("urn:uuid:" + postId);
entry.addChild("link")
.setAttribute("rel", "replies")
@ -880,11 +875,11 @@ public class IqGenerator extends AbstractGenerator {
options.putString("pubsub#type", Namespace.PUBSUB_SOCIAL_FEED);
options.putString("pubsub#access_model", "roster");
options.putString("pubsub#persist_items", "1");
options.putString("pubsub#deliver_payloads", "0");
options.putString("pubsub#send_last_published_item", "on_sub");
options.putString("pubsub#max_items", "max");
options.putString("pubsub#notify_retract", "1");
options.putString("pubsub#deliver_notifications", "1");
options.putString("pubsub#deliver_payloads", "1");
options.putString("pubsub#send_last_published_item", "never");
options.putString("pubsub#publish_model", "publishers");
return options;
}
@ -895,11 +890,11 @@ public class IqGenerator extends AbstractGenerator {
options.putString("pubsub#type", "urn:xmpp:microblog:0:comments");
options.putString("pubsub#access_model", "roster");
options.putString("pubsub#persist_items", "1");
options.putString("pubsub#max_items", "1000");
options.putString("pubsub#max_items", "max");
options.putString("pubsub#notify_retract", "1");
options.putString("pubsub#deliver_notifications", "1");
options.putString("pubsub#deliver_payloads", "1");
options.putString("pubsub#send_last_published_item", "never");
options.putString("pubsub#deliver_payloads", "0");
options.putString("pubsub#send_last_published_item", "on_sub");
options.putString("pubsub#publish_model", "open");
options.putString("pubsub#itemreply", "publisher");
return options;

View file

@ -9,6 +9,7 @@ import com.google.common.io.BaseEncoding;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Comment;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Post;
import eu.siacs.conversations.entities.Room;
@ -457,17 +458,27 @@ public class IqParser extends AbstractParser implements Consumer<Iq> {
}
}
}
} else if (node != null && node.startsWith("urn:xmpp:microblog:0") || node != null && node.startsWith(Namespace.PUBSUB_SOCIAL_FEED)) {
} else if (node != null && node.equals(Namespace.ATOM) || node != null && node.startsWith("urn:xmpp:microblog:0") || node != null && node.startsWith(Namespace.PUBSUB_SOCIAL_FEED)) {
for (Element child : items.getChildren()) {
if ("item".equals(child.getName())) {
final String postId = child.getAttribute("id");
Element entry = child.findChild("entry", Namespace.ATOM);
if (entry != null) {
try {
Post post = Post.fromElement(entry);
mXmppConnectionService.onPostReceived(post, account);
Element inReplyTo = entry.findChild("in-reply-to", "http://purl.org/syndication/thread/1.0");
if (inReplyTo != null) {
Comment comment = Comment.fromElement(entry);
String originalPostUuid = inReplyTo.getAttribute("ref");
if (originalPostUuid != null && originalPostUuid.startsWith("urn:uuid:")) {
originalPostUuid = originalPostUuid.substring(9);
}
mXmppConnectionService.notifyOnCommentReceived(originalPostUuid, comment);
} else {
Post post = Post.fromElement(entry);
mXmppConnectionService.onPostReceived(post, account);
}
} catch (Exception e) {
Log.e(Config.LOGTAG, "error creating post from pubsub item in iq", e);
Log.d(Config.LOGTAG, "error creating post/comment from pubsub item in iq", e);
}
} else if (postId != null) {
mXmppConnectionService.onPostRetracted(postId);

View file

@ -35,6 +35,7 @@ import java.util.function.Consumer;
import java.util.stream.Collectors;
import eu.siacs.conversations.crypto.OtrService;
import eu.siacs.conversations.entities.Comment;
import eu.siacs.conversations.entities.Post;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
@ -1759,26 +1760,31 @@ public class MessageParser extends AbstractParser
final Element items = event.findChild("items");
if (items != null) {
final String node = items.getAttribute("node");
if (node != null && node.startsWith("urn:xmpp:microblog:0") || node != null && node.startsWith(Namespace.PUBSUB_SOCIAL_FEED)) {
if (node != null && node.equals(Namespace.ATOM) || node != null && node.startsWith("urn:xmpp:microblog:0") || node != null && node.startsWith(Namespace.PUBSUB_SOCIAL_FEED)) {
for (Element child : items.getChildren()) {
if ("item".equals(child.getName())) {
final String postId = child.getAttribute("id");
Element entry = child.findChild("entry", Namespace.ATOM);
if (entry != null) {
try {
Post post = Post.fromElement(entry);
mXmppConnectionService.onPostReceived(post, account);
Element inReplyTo = entry.findChild("in-reply-to", "http://purl.org/syndication/thread/1.0");
if (inReplyTo != null) {
Comment comment = Comment.fromElement(entry);
String originalPostUuid = inReplyTo.getAttribute("ref");
if (originalPostUuid != null && originalPostUuid.startsWith("urn:uuid:")) {
originalPostUuid = originalPostUuid.substring(9);
}
mXmppConnectionService.notifyOnCommentReceived(originalPostUuid, comment);
} else {
Post post = Post.fromElement(entry);
mXmppConnectionService.onPostReceived(post, account);
}
} catch (Exception e) {
Log.d(Config.LOGTAG, "error creating post from pubsub item in message", e);
Log.d(Config.LOGTAG, "error creating post/comment from pubsub item in message", e);
}
} else if (postId != null) {
mXmppConnectionService.onPostRetracted(postId);
}
} else if ("retract".equals(child.getName())) {
final String postId = child.getAttribute("id");
if (postId != null) {
mXmppConnectionService.onPostRetracted(postId);
}
}
}
} else {

View file

@ -126,6 +126,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import eu.siacs.conversations.Conversations;
import eu.siacs.conversations.entities.Comment;
import eu.siacs.conversations.entities.Post;
import eu.siacs.conversations.entities.Story;
import eu.siacs.conversations.entities.StubConversation;
@ -8383,12 +8384,44 @@ public class XmppConnectionService extends Service {
}
public void subscribeTo(final Account account, final Jid to, final String node, final Consumer<Iq> callback) {
final Iq iq = getIqGenerator().generateSubscriptionIq(to, node, account.getJid());
final Iq iq = getIqGenerator().generateSubscriptionIq(to.asBareJid(), node, account.getJid().asBareJid());
sendIqPacket(account, iq, callback);
}
public void unsubscribeFrom(final Account account, final Jid to, final String node, final Consumer<Iq> callback) {
final Iq iq = getIqGenerator().generateUnsubscriptionIq(to, node, account.getJid());
final Iq iq = getIqGenerator().generateUnsubscriptionIq(to.asBareJid(), node, account.getJid().asBareJid());
sendIqPacket(account, iq, callback);
}
public interface OnCommentReceived {
void onCommentReceived(String originalPostUuid, Comment comment);
}
private final List<OnCommentReceived> mOnCommentReceivedListeners = new ArrayList<>();
public void addOnCommentReceivedListener(OnCommentReceived listener) {
synchronized (mOnCommentReceivedListeners) {
if (!mOnCommentReceivedListeners.contains(listener)) {
mOnCommentReceivedListeners.add(listener);
}
}
}
public void removeOnCommentReceivedListener(OnCommentReceived listener) {
synchronized (mOnCommentReceivedListeners) {
mOnCommentReceivedListeners.remove(listener);
}
}
public void notifyOnCommentReceived(String originalPostUuid, Comment comment) {
synchronized (mOnCommentReceivedListeners) {
for (OnCommentReceived listener : mOnCommentReceivedListeners) {
try {
listener.onCommentReceived(originalPostUuid, comment);
} catch (Exception e) {
Log.d(Config.LOGTAG, "safe to ignore, listener has been removed");
}
}
}
}
}

View file

@ -330,7 +330,7 @@ public class ContactDetailsActivity extends OmemoActivity
xmppConnectionService.subscribeTo(
contact.getAccount(),
contact.getJid(),
"urn:xmpp:microblog:0",
Namespace.MICROBLOG,
packet -> {
runOnUiThread(
() -> {
@ -359,7 +359,7 @@ public class ContactDetailsActivity extends OmemoActivity
xmppConnectionService.unsubscribeFrom(
contact.getAccount(),
contact.getJid(),
"urn:xmpp:microblog:0",
Namespace.MICROBLOG,
packet -> {
runOnUiThread(
() -> {

View file

@ -30,6 +30,7 @@ import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ActivityCreatePostBinding;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Namespace;
public class CreatePostActivity extends XmppActivity {
@ -319,7 +320,7 @@ public class CreatePostActivity extends XmppActivity {
}
private void publish(Account account, String title, String content, String attachmentUrl, String attachmentType) {
xmppConnectionService.publishPost(account, "urn:xmpp:microblog:0", title, content, attachmentUrl, attachmentType, postId, new XmppConnectionService.OnPostPublished() {
xmppConnectionService.publishPost(account, Namespace.MICROBLOG, title, content, attachmentUrl, attachmentType, postId, new XmppConnectionService.OnPostPublished() {
@Override
public void onPostPublished() {
runOnUiThread(() -> {

View file

@ -261,7 +261,7 @@ public class PostsActivity extends XmppActivity implements XmppConnectionService
}
for (Jid source : sourcesToFetch) {
xmppConnectionService.fetchPubsubItems(source, "urn:xmpp:microblog:0", new XmppConnectionService.OnPubsubItemsFetched() {
xmppConnectionService.fetchPubsubItems(source, Namespace.MICROBLOG, new XmppConnectionService.OnPubsubItemsFetched() {
@Override
public void onPubsubItemsFetched(String feedXml) {
try {

View file

@ -12,6 +12,8 @@ import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.text.DateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import eu.siacs.conversations.Config;
@ -34,12 +36,56 @@ public class CommentsAdapter extends RecyclerView.Adapter<CommentsAdapter.Commen
private final List<Comment> comments;
private final XmppActivity mActivity;
private XmppConnectionService.OnCommentReceived mOnCommentReceived;
public CommentsAdapter(XmppActivity activity, Post post, List<Comment> comments) {
this.mActivity = activity;
this.mPost = post;
this.comments = comments;
}
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
if (mActivity.xmppConnectionService != null) {
this.mOnCommentReceived = (postUuid, comment) -> {
if (mPost.getId().equals(postUuid)) {
mActivity.runOnUiThread(() -> {
boolean found = false;
for (Comment c : comments) {
if (c.getId().equals(comment.getId())) {
found = true;
break;
}
}
if (!found) {
comments.add(comment);
Collections.sort(comments, (c1, c2) -> {
Date d1 = c1.getPublished();
Date d2 = c2.getPublished();
if (d1 == null && d2 == null) return 0;
if (d1 == null) return -1;
if (d2 == null) return 1;
return d1.compareTo(d2);
});
notifyDataSetChanged();
}
});
}
};
mActivity.xmppConnectionService.addOnCommentReceivedListener(this.mOnCommentReceived);
}
}
@Override
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
super.onDetachedFromRecyclerView(recyclerView);
if (mActivity.xmppConnectionService != null && this.mOnCommentReceived != null) {
mActivity.xmppConnectionService.removeOnCommentReceivedListener(this.mOnCommentReceived);
}
}
@NonNull
@Override
public CommentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {

View file

@ -20,6 +20,7 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.PostsActivity;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.xml.Namespace;
import im.conversations.android.xmpp.model.stanza.Iq;
public class FollowSuggestionAdapter extends RecyclerView.Adapter<FollowSuggestionAdapter.ViewHolder> {
@ -49,7 +50,7 @@ public class FollowSuggestionAdapter extends RecyclerView.Adapter<FollowSuggesti
holder.mAvatar.setOnClickListener(v -> mPostsActivity.switchToContactDetails(contact));
holder.mFollowButton.setOnClickListener(v -> {
final Account account = contact.getAccount();
mXmppConnectionService.subscribeTo(account, contact.getJid(), "urn:xmpp:microblog:0", packet -> {
mXmppConnectionService.subscribeTo(account, contact.getJid(), Namespace.MICROBLOG, packet -> {
mPostsActivity.runOnUiThread(() -> {
if (packet.getType() == Iq.Type.RESULT) {
contact.setFollowed(true);

View file

@ -58,6 +58,7 @@ import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xml.XmlReader;
import eu.siacs.conversations.xmpp.Jid;
import io.noties.markwon.Markwon;
@ -309,7 +310,7 @@ public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.PostViewHold
.setTitle(R.string.retract_post)
.setMessage(R.string.retract_post_confirm)
.setPositiveButton(R.string.retract, (dialog, which) -> {
mActivity.xmppConnectionService.retractPost(ownAccount, "urn:xmpp:microblog:0", post.getId(),
mActivity.xmppConnectionService.retractPost(ownAccount, Namespace.MICROBLOG, post.getId(),
new XmppConnectionService.OnPostRetracted() {
@Override

View file

@ -112,7 +112,7 @@ public final class Namespace {
public static final String MDS_DISPLAYED = "urn:xmpp:mds:displayed:0";
public static final String MDS_SERVER_ASSIST = "urn:xmpp:mds:server-assist:0";
public static final String PUBSUB_SOCIAL_FEED = "urn:xmpp:pubsub-social-feed:1";
public static final String PUBSUB_MICROBLOG = "urn:xmpp:microblog:0";
public static final String MICROBLOG = "urn:xmpp:microblog:0";
public static final String PUBSUB_STORIES = "urn:xmpp:pubsub-social-feed:stories:0";
public static final String ATOM = "http://www.w3.org/2005/Atom";