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
8 changed files with 177 additions and 9 deletions
Showing only changes of commit e63f6e9ff7 - Show all commits

Implement live updates for microblog posts

Introduce listeners to receive and retract microblog posts in real-time. This allows the post view to update automatically when new posts are received or existing ones are retracted, without requiring a manual refresh.

Key changes include:
- Add `OnPostReceived` and `OnPostRetracted` listeners in `XmppConnectionService`.
- Parse incoming microblog posts from both IQ stanzas and pubsub event messages.
- Update the `PostsActivity` to reflect new and retracted posts instantly.
- Pass the `postId` on post retraction for more specific UI updates.
- Announce support for the `urn:xmpp:microblog:0` namespace.
Arne 2026-01-12 18:56:25 +01:00

View file

@ -116,7 +116,8 @@ 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("urn:xmpp:microblog:0");
features.add("urn:xmpp:microblog:0+notify");
features.add(Namespace.PUBSUB_STORIES);
features.add(Namespace.PUBSUB_STORIES + "+notify");
if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION) {

View file

@ -859,9 +859,29 @@ public class IqGenerator extends AbstractGenerator {
final String now = AbstractGenerator.getTimestamp(System.currentTimeMillis());
entry.addChild("published").setContent(now);
entry.addChild("updated").setContent(now);
// create a child node for the node
pubsub.addChild("create").setAttribute("node", "urn:xmpp:microblog:0");
final Element configure = pubsub.addChild("configure");
// Correctly use the 'pubsub#node_config' namespace
final Data data = Data.create("http://jabber.org/protocol/pubsub#node_config", defaultPostConfiguration());
configure.addChild(data);
return iq;
}
public static Bundle defaultPostConfiguration() {
Bundle options = new Bundle();
options.putString("pubsub#node_type", "leaf");
options.putString("pubsub#type", "urn:xmpp:microblog:0");
options.putString("pubsub#access_model", "roster");
options.putString("pubsub#item_expire", "86400");
options.putString("pubsub#persist_items", "1");
options.putString("pubsub#max_items", "120");
options.putString("pubsub#notify_retract", "1");
options.putString("pubsub#send_last_published_item", "on_sub");
options.putString("pubsub#publish_model", "publishers");
return options;
}
public Iq publishComment(final Account account, final String node, final String title, final String inReplyToId) {
final Iq iq = new Iq(Iq.Type.SET);
iq.setTo(account.getJid().asBareJid());

View file

@ -10,6 +10,7 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Post;
import eu.siacs.conversations.entities.Room;
import eu.siacs.conversations.entities.Story;
import eu.siacs.conversations.services.XmppConnectionService;
@ -456,6 +457,29 @@ public class IqParser extends AbstractParser implements Consumer<Iq> {
}
}
}
} else if ("urn:xmpp:microblog:0".equals(node)) {
for (Element item : items.getChildren()) {
if ("item".equals(item.getName())) {
Element entry = item.findChild("entry", Namespace.ATOM);
if (entry != null) {
try {
Post post = Post.fromElement(entry);
mXmppConnectionService.onPostReceived(post, account);
if (mXmppConnectionService.getOnPostReceivedListener() != null) {
mXmppConnectionService.getOnPostReceivedListener().onPostReceived(post);
}
} catch (Exception e) {
Log.e(
Config.LOGTAG,
"error creating post from pubsub item in iq",
e);
}
}
} else if (item.getName().equals("retract")) {
final String postId = item.getAttribute("id");
mXmppConnectionService.onPostRetracted(postId);
}
}
}
}
} else if ((packet.hasChild("block", Namespace.BLOCKING)

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.Post;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.entities.Story;
@ -1756,8 +1757,29 @@ public class MessageParser extends AbstractParser
final Element event =
original.findChild("event", "http://jabber.org/protocol/pubsub#event");
if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) {
if (event.hasChild("items")) {
parseEvent(event, original.getFrom(), account);
final Element items = event.findChild("items");
if (items != null) {
final String node = items.getAttribute("node");
if ("urn:xmpp:microblog:0".equals(node)) {
for (Element item : items.getChildren()) {
if ("item".equals(item.getName())) {
Element entry = item.findChild("entry", Namespace.ATOM);
if (entry != null) {
try {
Post post = Post.fromElement(entry);
mXmppConnectionService.databaseBackend.createPost(post, account);
if (mXmppConnectionService.getOnPostReceivedListener() != null) {
mXmppConnectionService.getOnPostReceivedListener().onPostReceived(post);
}
} catch (Exception e) {
Log.d(Config.LOGTAG, "error creating post from pubsub item", e);
}
}
}
}
} else {
parseEvent(event, original.getFrom(), account);
}
} else if (event.hasChild("delete")) {
parseDeleteEvent(event, original.getFrom(), account);
} else if (event.hasChild("purge")) {

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.Post;
import eu.siacs.conversations.entities.Story;
import eu.siacs.conversations.entities.StubConversation;
import eu.siacs.conversations.utils.TranscoderStrategies;
@ -8227,7 +8228,7 @@ public class XmppConnectionService extends Service {
sendIqPacket(account, request, response -> {
if (response.getType() == Iq.Type.RESULT) {
if (callback != null) {
callback.onPostRetracted();
callback.onPostRetracted(id);
}
} else {
if (callback != null) {
@ -8242,8 +8243,52 @@ public class XmppConnectionService extends Service {
void onPostPublishFailed();
}
public interface OnPostReceived {
void onPostReceived(Post post);
}
private OnPostReceived mOnPostReceivedListener;
public void setOnPostReceivedListener(OnPostReceived listener) {
this.mOnPostReceivedListener = listener;
}
public OnPostReceived getOnPostReceivedListener() {
return this.mOnPostReceivedListener;
}
public void onPostReceived(Post post, Account account) {
if (post == null || account == null) {
return;
}
databaseBackend.createPost(post, account);
if (mOnPostReceivedListener != null) {
mOnPostReceivedListener.onPostReceived(post);
}
}
public interface OnPostRetracted {
void onPostRetracted();
void onPostRetracted(String postId);
void onPostRetractionFailed();
}
private OnPostRetracted mOnPostRetractedListener;
public void setOnPostRetractedListener(OnPostRetracted listener) {
this.mOnPostRetractedListener = listener;
}
public OnPostRetracted getOnPostRetractedListener() {
return this.mOnPostRetractedListener;
}
public void onPostRetracted(String postId) {
if (postId == null) {
return;
}
databaseBackend.deletePost(postId);
if (mOnPostRetractedListener != null) {
mOnPostRetractedListener.onPostRetracted(postId);
}
}
}

View file

@ -34,6 +34,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 {

View file

@ -17,6 +17,9 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@ -34,7 +37,7 @@ import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xml.XmlReader;
import eu.siacs.conversations.xmpp.Jid;
public class PostsActivity extends XmppActivity {
public class PostsActivity extends XmppActivity implements XmppConnectionService.OnPostReceived, XmppConnectionService.OnPostRetracted {
private ActivityPostsBinding binding;
private PostsAdapter postsAdapter;
@ -44,6 +47,7 @@ public class PostsActivity extends XmppActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_posts);
Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
setSupportActionBar(binding.toolbar);
configureActionBar(getSupportActionBar());
@ -93,6 +97,10 @@ public class PostsActivity extends XmppActivity {
@Override
public void onStart() {
super.onStart();
if (xmppConnectionService != null) {
xmppConnectionService.setOnPostReceivedListener(this);
xmppConnectionService.setOnPostRetractedListener(this);
}
if (postList.isEmpty()) {
loadPosts();
}
@ -106,11 +114,40 @@ public class PostsActivity extends XmppActivity {
}
}
@Override
public void onStop() {
super.onStop();
if (xmppConnectionService != null) {
xmppConnectionService.setOnPostReceivedListener(null);
xmppConnectionService.setOnPostRetractedListener(null);
}
}
@Override
public void onBackendConnected() {
if (xmppConnectionService != null) {
xmppConnectionService.setOnPostReceivedListener(this);
xmppConnectionService.setOnPostRetractedListener(this);
}
refreshUiReal();
}
@Override
public void onPostReceived(final Post post) {
runOnUiThread(() -> {
if (post != null) {
postList.add(0, post);
java.util.Collections.sort(postList, (p1, p2) -> {
if (p1.getPublished() == null && p2.getPublished() == null) return 0;
if (p1.getPublished() == null) return 1;
if (p2.getPublished() == null) return -1;
return p2.getPublished().compareTo(p1.getPublished());
});
postsAdapter.notifyDataSetChanged();
}
});
}
@Override
protected void refreshUiReal() {
if (postList.isEmpty()) {
@ -262,4 +299,21 @@ public class PostsActivity extends XmppActivity {
super.onBackPressed();
}
}
@Override
public void onPostRetracted(String postId) {
runOnUiThread(() -> {
for (int i = 0; i < postList.size(); ++i) {
if (postList.get(i).getId().equals(postId)) {
postList.remove(i);
postsAdapter.notifyItemRemoved(i);
break;
}
}
});
}
@Override
public void onPostRetractionFailed() {
runOnUiThread(() -> Toast.makeText(this, R.string.error_retract_post, Toast.LENGTH_SHORT).show());
}
}

View file

@ -258,10 +258,11 @@ public class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.PostViewHold
.setPositiveButton(R.string.retract, (dialog, which) -> {
mActivity.xmppConnectionService.retractPost(ownAccount, "urn:xmpp:microblog:0", post.getId(),
new XmppConnectionService.OnPostRetracted() {
@Override
public void onPostRetracted() {
public void onPostRetracted(String postId) {
mActivity.runOnUiThread(() -> {
mActivity.xmppConnectionService.databaseBackend.deletePost(post.getId());
mActivity.xmppConnectionService.databaseBackend.deletePost(postId);
int pos = getAdapterPosition();
if (pos != RecyclerView.NO_POSITION) {
posts.remove(pos);