diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 8bb65cc0f..587bfc496 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.common.collect.ComparisonChain; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import org.json.JSONArray; @@ -437,6 +438,17 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return null; } + public Message findReceivedWithRemoteId(final String id) { + synchronized (this.messages) { + for (final Message message : this.messages) { + if (message.getStatus() == Message.STATUS_RECEIVED && id.equals(message.getRemoteMsgId())) { + return message; + } + } + } + return null; + } + public Message findMessageWithServerMsgId(String id) { synchronized (this.messages) { for (Message message : this.messages) { @@ -576,20 +588,20 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } } - public List markRead(String upToUuid) { - final List unread = new ArrayList<>(); + public List markRead(final String upToUuid) { + final ImmutableList.Builder unread = new ImmutableList.Builder<>(); synchronized (this.messages) { - for (Message message : this.messages) { + for (final Message message : this.messages) { if (!message.isRead()) { message.markRead(); unread.add(message); } if (message.getUuid().equals(upToUuid)) { - return unread; + return unread.build(); } } } - return unread; + return unread.build(); } public Message getLatestMessage() { diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 42fc3c00f..3f63edbd7 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -2,6 +2,15 @@ package eu.siacs.conversations.generator; import android.util.Base64; +import eu.siacs.conversations.BuildConfig; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.XmppConnection; + import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; @@ -12,54 +21,42 @@ import java.util.List; import java.util.Locale; import java.util.TimeZone; -import eu.siacs.conversations.BuildConfig; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.PhoneHelper; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; - public abstract class AbstractGenerator { - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + private static final SimpleDateFormat DATE_FORMAT = + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); private final String[] FEATURES = { - Namespace.JINGLE, - Namespace.JINGLE_APPS_FILE_TRANSFER, - Namespace.JINGLE_TRANSPORTS_S5B, - Namespace.JINGLE_TRANSPORTS_IBB, - Namespace.JINGLE_ENCRYPTED_TRANSPORT, - Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO, - "http://jabber.org/protocol/muc", - "jabber:x:conference", - Namespace.OOB, - "http://jabber.org/protocol/caps", - "http://jabber.org/protocol/disco#info", - "urn:xmpp:avatar:metadata+notify", - Namespace.NICK + "+notify", - "urn:xmpp:ping", - "jabber:iq:version", - "http://jabber.org/protocol/chatstates" + Namespace.JINGLE, + Namespace.JINGLE_APPS_FILE_TRANSFER, + Namespace.JINGLE_TRANSPORTS_S5B, + Namespace.JINGLE_TRANSPORTS_IBB, + Namespace.JINGLE_ENCRYPTED_TRANSPORT, + Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO, + "http://jabber.org/protocol/muc", + "jabber:x:conference", + Namespace.OOB, + "http://jabber.org/protocol/caps", + "http://jabber.org/protocol/disco#info", + "urn:xmpp:avatar:metadata+notify", + Namespace.NICK + "+notify", + "urn:xmpp:ping", + "jabber:iq:version", + "http://jabber.org/protocol/chatstates", + Namespace.MDS_DISPLAYED + "+notify" }; private final String[] MESSAGE_CONFIRMATION_FEATURES = { - "urn:xmpp:chat-markers:0", - "urn:xmpp:receipts" - }; - private final String[] MESSAGE_CORRECTION_FEATURES = { - "urn:xmpp:message-correct:0" + "urn:xmpp:chat-markers:0", "urn:xmpp:receipts" }; + private final String[] MESSAGE_CORRECTION_FEATURES = {"urn:xmpp:message-correct:0"}; private final String[] PRIVACY_SENSITIVE = { - "urn:xmpp:time" //XEP-0202: Entity Time leaks time zone + "urn:xmpp:time" // XEP-0202: Entity Time leaks time zone }; private final String[] VOIP_NAMESPACES = { - Namespace.JINGLE_TRANSPORT_ICE_UDP, - Namespace.JINGLE_FEATURE_AUDIO, - Namespace.JINGLE_FEATURE_VIDEO, - Namespace.JINGLE_APPS_RTP, - Namespace.JINGLE_APPS_DTLS, - Namespace.JINGLE_MESSAGE + Namespace.JINGLE_TRANSPORT_ICE_UDP, + Namespace.JINGLE_FEATURE_AUDIO, + Namespace.JINGLE_FEATURE_VIDEO, + Namespace.JINGLE_APPS_RTP, + Namespace.JINGLE_APPS_DTLS, + Namespace.JINGLE_MESSAGE }; protected XmppConnectionService mXmppConnectionService; @@ -90,7 +87,11 @@ public abstract class AbstractGenerator { String getCapHash(final Account account) { StringBuilder s = new StringBuilder(); - s.append("client/").append(getIdentityType()).append("//").append(getIdentityName()).append('<'); + s.append("client/") + .append(getIdentityType()) + .append("//") + .append(getIdentityName()) + .append('<'); MessageDigest md; try { md = MessageDigest.getInstance("SHA-1"); diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index c9fa7f6a6..df87932e5 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -129,6 +129,10 @@ public class IqGenerator extends AbstractGenerator { return retrieve(Namespace.BOOKMARKS2, null); } + public IqPacket retrieveMds() { + return retrieve(Namespace.MDS_DISPLAYED, null); + } + public IqPacket publishNick(String nick) { final Element item = new Element("item"); item.setAttribute("id", "current"); @@ -264,6 +268,24 @@ public class IqGenerator extends AbstractGenerator { return conference; } + public Element mdsDisplayed(final String stanzaId, final Conversation conversation) { + final Jid by; + if (conversation.getMode() == Conversation.MODE_MULTI) { + by = conversation.getJid().asBareJid(); + } else { + by = conversation.getAccount().getJid().asBareJid(); + } + return mdsDisplayed(stanzaId, by); + } + + private Element mdsDisplayed(final String stanzaId, final Jid by) { + final Element displayed = new Element("displayed", Namespace.MDS_DISPLAYED); + final Element stanzaIdElement = displayed.addChild("stanza-id", Namespace.STANZA_IDS); + stanzaIdElement.setAttribute("id", stanzaId); + stanzaIdElement.setAttribute("by", by); + return displayed; + } + public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey, final Set preKeyRecords, final int deviceId, Bundle publishOptions) { final Element item = new Element("item"); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index cb0620fa1..ece8a7862 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -271,6 +271,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece mXmppConnectionService.updateConversationUi(); } } + } else if (Namespace.MDS_DISPLAYED.equals(node) && account.getJid().asBareJid().equals(from)) { + final Element item = items.findChild("item"); + mXmppConnectionService.processMdsItem(account, item); } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + " received pubsub notification for node=" + node); } @@ -985,12 +988,18 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } } - Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0"); + final Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0"); if (displayed != null) { final String id = displayed.getAttribute("id"); final Jid sender = InvalidJid.getNullForInvalid(displayed.getAttributeAsJid("sender")); if (packet.fromAccount(account) && !selfAddressed) { - dismissNotification(account, counterpart, query, id); + final Conversation c = + mXmppConnectionService.find(account, counterpart.asBareJid()); + final Message message = + (c == null || id == null) ? null : c.findReceivedWithRemoteId(id); + if (message != null && (query == null || query.isCatchup())) { + mXmppConnectionService.markReadUpTo(c, message); + } if (query == null) { activateGracePeriod(account); } @@ -1012,7 +1021,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid()); if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) { if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections - mXmppConnectionService.markRead(conversation); + mXmppConnectionService.markReadUpTo(conversation, message); } } else if (!counterpart.isBareJid() && trueJid != null) { final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index a28b2a484..bd9363696 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -49,6 +49,7 @@ import android.util.Pair; import androidx.annotation.BoolRes; import androidx.annotation.IntegerRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.app.RemoteInput; import androidx.core.content.ContextCompat; import androidx.core.util.Consumer; @@ -56,6 +57,8 @@ import androidx.core.util.Consumer; import com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import com.google.common.collect.Iterables; import org.conscrypt.Conscrypt; import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep; @@ -152,6 +155,7 @@ import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.LocalizedContent; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnBindListener; import eu.siacs.conversations.xmpp.OnContactStatusChanged; @@ -368,6 +372,12 @@ public class XmppConnectionService extends Service { } else if (!account.getXmppConnection().getFeatures().bookmarksConversion()) { fetchBookmarks(account); } + + if (connection.getFeatures().mds()) { + fetchMessageDisplayedSynchronization(account); + } else { + Log.d(Config.LOGTAG,account.getJid()+": server has no support for mds"); + } final boolean flexible = account.getXmppConnection().getFeatures().flexibleOfflineMessageRetrieval(); final boolean catchup = getMessageArchiveService().inCatchup(account); final boolean trackOfflineMessageRetrieval; @@ -392,6 +402,7 @@ public class XmppConnectionService extends Service { unifiedPushBroker.renewUnifiedPushEndpointsOnBind(account); } }; + private final AtomicLong mLastExpiryRun = new AtomicLong(0); private final LruCache, ServiceDiscoveryResult> discoCache = new LruCache<>(20); private final OnStatusChanged statusListener = new OnStatusChanged() { @@ -1902,18 +1913,88 @@ public class XmppConnectionService extends Service { public void fetchBookmarks2(final Account account) { final IqPacket retrieve = mIqGenerator.retrieveBookmarks(); - sendIqPacket(account, retrieve, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, final IqPacket response) { - if (response.getType() == IqPacket.TYPE.RESULT) { - final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB); - final Map bookmarks = Bookmark.parseFromPubsub(pubsub, account); - processBookmarksInitial(account, bookmarks, true); - } + sendIqPacket(account, retrieve, (a, response) -> { + if (response.getType() == IqPacket.TYPE.RESULT) { + final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB); + final Map bookmarks = Bookmark.parseFromPubsub(pubsub, a); + processBookmarksInitial(a, bookmarks, true); } }); } + private void fetchMessageDisplayedSynchronization(final Account account) { + Log.d(Config.LOGTAG, account.getJid() + ": retrieve mds"); + final var retrieve = mIqGenerator.retrieveMds(); + sendIqPacket( + account, + retrieve, + (a, response) -> { + if (response.getType() != IqPacket.TYPE.RESULT) { + return; + } + final var pubSub = response.findChild("pubsub", Namespace.PUBSUB); + final Element items = pubSub == null ? null : pubSub.findChild("items"); + if (items == null + || !Namespace.MDS_DISPLAYED.equals(items.getAttribute("node"))) { + return; + } + for (final Element child : items.getChildren()) { + if ("item".equals(child.getName())) { + processMdsItem(account, child); + } + } + }); + } + + public void processMdsItem(final Account account, final Element item) { + final Jid jid = + item == null ? null : InvalidJid.getNullForInvalid(item.getAttributeAsJid("id")); + if (jid == null) { + return; + } + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": processing mds item for " + jid); + final Element displayed = item.findChild("displayed", Namespace.MDS_DISPLAYED); + final Element stanzaId = + displayed == null ? null : displayed.findChild("stanza-id", Namespace.STANZA_IDS); + final String id = stanzaId == null ? null : stanzaId.getAttribute("id"); + final Conversation conversation = find(account, jid); + if (id != null && conversation != null) { + markReadUpToStanzaId(conversation, id); + } + } + + public void markReadUpToStanzaId(final Conversation conversation, final String stanzaId) { + final Message message = conversation.findMessageWithServerMsgId(stanzaId); + if (message == null) { // do we want to check if isRead? + return; + } + markReadUpTo(conversation, message); + } + + public void markReadUpTo(final Conversation conversation, final Message message) { + final boolean isDismissNotification = isDismissNotification(message); + final var uuid = message.getUuid(); + Log.d( + Config.LOGTAG, + conversation.getAccount().getJid().asBareJid() + + ": mark " + + conversation.getJid().asBareJid() + + " as read up to " + + uuid); + markRead(conversation, uuid, isDismissNotification); + } + + private static boolean isDismissNotification(final Message message) { + Message next = message.next(); + while (next != null) { + if (message.getStatus() == Message.STATUS_RECEIVED) { + return false; + } + next = next.next(); + } + return true; + } + public void processBookmarksInitial(Account account, Map bookmarks, final boolean pep) { final Set previousBookmarks = account.getBookmarkedJids(); final boolean synchronizeWithBookmarks = synchronizeWithBookmarks(); @@ -2050,7 +2131,7 @@ public class XmppConnectionService extends Service { } }); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error publishing bookmarks (retry=" + retry + ") " + response); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error publishing "+node+" (retry=" + retry + ") " + response); } }); } @@ -4534,24 +4615,101 @@ public class XmppConnectionService extends Service { } } - public void sendReadMarker(final Conversation conversation, String upToUuid) { - final boolean isPrivateAndNonAnonymousMuc = conversation.getMode() == Conversation.MODE_MULTI && conversation.isPrivateAndNonAnonymous(); + public void sendReadMarker(final Conversation conversation, final String upToUuid) { + final boolean isPrivateAndNonAnonymousMuc = + conversation.getMode() == Conversation.MODE_MULTI + && conversation.isPrivateAndNonAnonymous(); final List readMessages = this.markRead(conversation, upToUuid, true); - if (readMessages.size() > 0) { - updateConversationUi(); + if (readMessages.isEmpty()) { + return; } - final Message markable = Conversation.getLatestMarkableMessage(readMessages, isPrivateAndNonAnonymousMuc); - if (confirmMessages() - && markable != null - && (markable.trusted() || isPrivateAndNonAnonymousMuc) - && markable.getRemoteMsgId() != null) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": sending read marker to " + markable.getCounterpart().toString()); - final Account account = conversation.getAccount(); - final MessagePacket packet = mMessageGenerator.confirm(markable); + final var account = conversation.getAccount(); + final var connection = account.getXmppConnection(); + updateConversationUi(); + final var last = + Iterables.getLast( + Collections2.filter( + readMessages, + m -> + !m.isPrivateMessage() + && m.getStatus() == Message.STATUS_RECEIVED), + null); + if (last == null) { + return; + } + + final boolean sendDisplayedMarker = + confirmMessages() + && (last.trusted() || isPrivateAndNonAnonymousMuc) + && last.getRemoteMsgId() != null + && (last.markable || isPrivateAndNonAnonymousMuc); + final boolean serverAssist = + connection != null && connection.getFeatures().mdsServerAssist(); + + final String stanzaId = last.getServerMsgId(); + + if (sendDisplayedMarker && serverAssist) { + final var mdsDisplayed = mIqGenerator.mdsDisplayed(stanzaId, conversation); + final MessagePacket packet = mMessageGenerator.confirm(last); + packet.addChild(mdsDisplayed); + if (!last.isPrivateMessage()) { + packet.setTo(packet.getTo().asBareJid()); + } + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": server assisted "+packet); this.sendMessagePacket(account, packet); + } else { + publishMds(last); + // read markers will be sent after MDS to flush the CSI stanza queue + if (sendDisplayedMarker) { + Log.d( + Config.LOGTAG, + conversation.getAccount().getJid().asBareJid() + + ": sending displayed marker to " + + last.getCounterpart().toString()); + final MessagePacket packet = mMessageGenerator.confirm(last); + this.sendMessagePacket(account, packet); + } } } + private void publishMds(@Nullable final Message message) { + final String stanzaId = message == null ? null : message.getServerMsgId(); + if (Strings.isNullOrEmpty(stanzaId)) { + return; + } + final Conversation conversation; + final var conversational = message.getConversation(); + if (conversational instanceof Conversation c) { + conversation = c; + } else { + return; + } + final var account = conversation.getAccount(); + final var connection = account.getXmppConnection(); + if (connection == null || !connection.getFeatures().mds()) { + return; + } + final Jid itemId; + if (message.isPrivateMessage()) { + itemId = message.getCounterpart(); + } else { + itemId = conversation.getJid().asBareJid(); + } + Log.d(Config.LOGTAG,"publishing mds for "+itemId+"/"+stanzaId); + publishMds(account, itemId, stanzaId, conversation); + } + + private void publishMds( + final Account account, final Jid itemId, final String stanzaId, final Conversation conversation) { + final var item = mIqGenerator.mdsDisplayed(stanzaId, conversation); + pushNodeAndEnforcePublishOptions( + account, + Namespace.MDS_DISPLAYED, + item, + itemId.toEscapedString(), + PublishOptions.persistentWhitelistAccessMaxItems()); + } + public MemorizingTrustManager getMemorizingTrustManager() { return this.mMemorizingTrustManager; } diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 85714c765..2e5eb1c5a 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -24,6 +24,7 @@ public final class Namespace { public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls"; public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options"; + public static final String PUBSUB_CONFIG_NODE_MAX = PUBSUB + "#config-node-max"; public static final String PUBSUB_ERROR = PUBSUB + "#errors"; public static final String PUBSUB_OWNER = PUBSUB + "#owner"; public static final String NICK = "http://jabber.org/protocol/nick"; @@ -76,4 +77,6 @@ public final class Namespace { public static final String REPORTING_REASON_SPAM = "urn:xmpp:reporting:spam"; public static final String SDP_OFFER_ANSWER = "urn:ietf:rfc:3264"; public static final String HASHES = "urn:xmpp:hashes:2"; + public static final String MDS_DISPLAYED = "urn:xmpp:mds:displayed:0"; + public static final String MDS_SERVER_ASSIST = "urn:xmpp:mds:server-assist:0"; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index cba12e19e..b29129ee2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -2968,6 +2968,10 @@ public class XmppConnection implements Runnable { return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_PUBLISH_OPTIONS); } + public boolean pepConfigNodeMax() { + return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_CONFIG_NODE_MAX); + } + public boolean pepOmemoWhitelisted() { return hasDiscoFeature( account.getJid().asBareJid(), AxolotlService.PEP_OMEMO_WHITELISTED); @@ -3068,5 +3072,13 @@ public class XmppConnection implements Runnable { public boolean externalServiceDiscovery() { return hasDiscoFeature(account.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY); } + + public boolean mds() { + return pepPublishOptions() && pepConfigNodeMax(); + } + + public boolean mdsServerAssist() { + return hasDiscoFeature(account.getJid().asBareJid(), Namespace.MDS_DISPLAYED); + } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java b/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java index 24b429fd7..ef1da8561 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java @@ -31,7 +31,6 @@ public class PublishOptions { options.putString("pubsub#access_model", "whitelist"); options.putString("pubsub#send_last_published_item", "never"); options.putString("pubsub#max_items", "max"); - options.putString("pubsub#notify_delete", "true"); options.putString("pubsub#notify_retract", "true"); //one could also set notify=true on the retract