Detailed changes
@@ -60,6 +60,13 @@
<implements rdf:resource="https://xmpp.org/rfcs/rfc6122.html"/>
<implements rdf:resource="https://xmpp.org/rfcs/rfc7590.html"/>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0425.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.2.1</xmpp:version>
+ </xmpp:SupportedXep>
+ </implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0428.html"/>
@@ -31,4 +31,7 @@
<string name="pref_dialler_integration_incoming_summary">Incoming calls from phone numbers may ring with your system dialler instead of this app\'s notification settings</string>
<string name="save_as_sticker">Save as Sticker</string>
<string name="sticker_name">Sticker Name</string>
+ <string name="moderate_message">Moderate</string>
+ <string name="moderate_reason">Moderation Resaon</string>
+ <string name="unable_to_moderate">Unable to Moderate</string>
</resources>
@@ -478,7 +478,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return null;
}
- public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
+ public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart) {
synchronized (this.messages) {
for (int i = this.messages.size() - 1; i >= 0; --i) {
final Message message = messages.get(i);
@@ -486,14 +486,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
if (mcp == null) {
continue;
}
- if (mcp.equals(counterpart) && ((message.getStatus() == Message.STATUS_RECEIVED) == received)
- && (carbon == message.isCarbon() || received)) {
- final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id);
- if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) {
- return message;
- } else {
- return null;
- }
+ if (mcp.equals(counterpart) || mcp.asBareJid().equals(counterpart)) {
+ final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id) || (getMode() == MODE_MULTI && id.equals(message.getServerMsgId()));
+ if (idMatch) return message;
}
}
}
@@ -981,6 +981,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
}
+ public void clearPayloads() {
+ this.payloads.clear();
+ }
+
public void addPayload(Element el) {
if (el == null) return;
@@ -1080,7 +1084,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
public synchronized void setFileParams(FileParams fileParams) {
- if (this.fileParams != null && this.fileParams.sims != null && fileParams.sims == null) {
+ if (fileParams != null && this.fileParams != null && this.fileParams.sims != null && fileParams.sims == null) {
fileParams.sims = this.fileParams.sims;
}
this.fileParams = fileParams;
@@ -36,6 +36,7 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Bookmark;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
@@ -410,6 +411,19 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
+ public IqPacket moderateMessage(Account account, Message m, String reason) {
+ IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+ packet.setTo(m.getConversation().getJid().asBareJid());
+ packet.setFrom(account.getJid());
+ Element moderate =
+ packet.addChild("apply-to", "urn:xmpp:fasten:0")
+ .setAttribute("id", m.getServerMsgId())
+ .addChild("moderate", "urn:xmpp:message-moderate:0");
+ moderate.addChild("retract", "urn:xmpp:message-retract:0");
+ moderate.addChild("reason", "urn:xmpp:message-moderate:0").setContent(reason);
+ return packet;
+ }
+
public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
packet.setTo(host);
@@ -429,11 +429,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
}
String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
+ boolean replaceAsRetraction = false;
if (replacementId == null) {
Element fasten = packet.findChild("apply-to", "urn:xmpp:fasten:0");
- if (fasten != null && (fasten.findChild("retract", "urn:xmpp:message-retract:0") != null || fasten.findChild("urn:xmpp:message-moderate:0") != null)) {
+ if (fasten != null && (fasten.findChild("retract", "urn:xmpp:message-retract:0") != null || fasten.findChild("moderated", "urn:xmpp:message-moderate:0") != null)) {
replacementId = fasten.getAttribute("id");
packet.setBody("");
+ replaceAsRetraction = true;
}
}
final LocalizedContent body = packet.getBody();
@@ -671,10 +673,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
- final Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId,
- counterpart,
- message.getStatus() == Message.STATUS_RECEIVED,
- message.isCarbon());
+ final Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId, counterpart);
if (replacedMessage != null) {
final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null
|| replacedMessage.getFingerprint().equals(message.getFingerprint());
@@ -683,7 +682,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
&& replacedMessage.getTrueCounterpart().asBareJid().equals(message.getTrueCounterpart().asBareJid());
final boolean mucUserMatches = query == null && replacedMessage.sameMucUser(message); //can not be checked when using mam
final boolean duplicate = conversation.hasDuplicateMessage(message);
- if (fingerprintsMatch && (trueCountersMatch || !conversationMultiMode || mucUserMatches) && !duplicate) {
+ if (fingerprintsMatch && (trueCountersMatch || !conversationMultiMode || mucUserMatches || counterpart.isBareJid()) && !duplicate) {
Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'");
synchronized (replacedMessage) {
final String uuid = replacedMessage.getUuid();
@@ -691,6 +690,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
replacedMessage.setBody(message.getBody());
replacedMessage.putEdited(replacedMessage.getRemoteMsgId(), replacedMessage.getServerMsgId());
replacedMessage.setRemoteMsgId(remoteMsgId);
+ if (replaceAsRetraction) {
+ mXmppConnectionService.getFileBackend().deleteFile(replacedMessage);
+ mXmppConnectionService.evictPreview(message.getUuid());
+ replacedMessage.clearPayloads();
+ replacedMessage.setFileParams(null);
+ replacedMessage.setDeleted(true);
+ }
if (replacedMessage.getServerMsgId() == null || message.getServerMsgId() != null) {
replacedMessage.setServerMsgId(message.getServerMsgId());
}
@@ -3555,6 +3555,18 @@ public class XmppConnectionService extends Service {
});
}
+ public void moderateMessage(final Account account, final Message m, final String reason) {
+ IqPacket request = this.mIqGenerator.moderateMessage(account, m, reason);
+ Log.d(Config.LOGTAG, "moderate: " + request);
+ sendIqPacket(account, request, (a, packet) -> {
+ Log.d(Config.LOGTAG, "moderate1: " + packet);
+ if (packet.getType() != IqPacket.TYPE.RESULT) {
+ showErrorToastInUi(R.string.unable_to_moderate);
+ Log.d(Config.LOGTAG, a.getJid().asBareJid() + " unable to moderate: " + packet);
+ }
+ });
+ }
+
public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) {
IqPacket request = new IqPacket(IqPacket.TYPE.SET);
request.setTo(conversation.getJid().asBareJid());
@@ -1002,7 +1002,7 @@ public class ConversationFragment extends XmppFragment
final String existingName = savingAsStickerName;
savingAsSticker = null;
savingAsStickerName = null;
- activity.quickEdit(existingName, R.string.sticker_name, (name) -> {
+ activity.quickEdit(existingName, (name) -> {
try {
activity.xmppConnectionService.getFileBackend().copyFileToDocumentFile(activity, f, df, name);
} catch (final FileBackend.FileCopyException e) {
@@ -1012,7 +1012,7 @@ public class ConversationFragment extends XmppFragment
Toast.makeText(activity, "Sticker saved", Toast.LENGTH_SHORT).show();
return null;
- });
+ }, R.string.sticker_name, false, false, true);
break;
case REQUEST_TRUST_KEYS_TEXT:
sendMessage();
@@ -1431,6 +1431,7 @@ public class ConversationFragment extends XmppFragment
MenuItem retryDecryption = menu.findItem(R.id.retry_decryption);
MenuItem correctMessage = menu.findItem(R.id.correct_message);
MenuItem retractMessage = menu.findItem(R.id.retract_message);
+ MenuItem moderateMessage = menu.findItem(R.id.moderate_message);
MenuItem onlyThisThread = menu.findItem(R.id.only_this_thread);
MenuItem shareWith = menu.findItem(R.id.share_with);
MenuItem sendAgain = menu.findItem(R.id.send_again);
@@ -1461,6 +1462,9 @@ public class ConversationFragment extends XmppFragment
correctMessage.setVisible(true);
if (!relevantForCorrection.getBody().equals("") && !relevantForCorrection.getBody().equals(" ")) retractMessage.setVisible(true);
}
+ if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getMucOptions().getSelf().getRole().ranks(MucOptions.Role.MODERATOR) && conversation.getMucOptions().hasFeature("urn:xmpp:message-moderate:0")) {
+ moderateMessage.setVisible(true);
+ }
if ((m.isFileOrImage() && !deleted && !receiving)
|| (m.getType() == Message.TYPE_TEXT && !m.treatAsDownloadable())
&& !unInitiatedButKnownSize
@@ -1543,6 +1547,12 @@ public class ConversationFragment extends XmppFragment
})
.setNegativeButton(R.string.no, null).show();
return true;
+ case R.id.moderate_message:
+ activity.quickEdit("Spam", (reason) -> {
+ activity.xmppConnectionService.moderateMessage(conversation.getAccount(), selectedMessage, reason);
+ return null;
+ }, R.string.moderate_reason, false, false, true);
+ return true;
case R.id.copy_message:
ShareUtil.copyToClipboard(activity, selectedMessage);
return true;
@@ -722,12 +722,17 @@ public abstract class XmppActivity extends ActionBarActivity {
quickEdit(previousValue, callback, R.string.password, true, false);
}
+ protected void quickEdit(final String previousValue, final OnValueEdited callback, final @StringRes int hint, boolean password, boolean permitEmpty) {
+ quickEdit(previousValue, callback, hint, password, permitEmpty, false);
+ }
+
@SuppressLint("InflateParams")
- private void quickEdit(final String previousValue,
+ protected void quickEdit(final String previousValue,
final OnValueEdited callback,
final @StringRes int hint,
boolean password,
- boolean permitEmpty) {
+ boolean permitEmpty,
+ boolean alwaysCallback) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
DialogQuickeditBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_quickedit, null, false);
if (password) {
@@ -748,7 +753,7 @@ public abstract class XmppActivity extends ActionBarActivity {
dialog.show();
View.OnClickListener clickListener = v -> {
String value = binding.inputEditText.getText().toString();
- if (!value.equals(previousValue) && (!value.trim().isEmpty() || permitEmpty)) {
+ if ((alwaysCallback || !value.equals(previousValue)) && (!value.trim().isEmpty() || permitEmpty)) {
String error = callback.onValueEdited(value);
if (error != null) {
binding.inputLayout.setError(error);
@@ -38,6 +38,10 @@
android:id="@+id/retract_message"
android:title="@string/retract_message"
android:visible="false" />
+ <item
+ android:id="@+id/moderate_message"
+ android:title="@string/moderate_message"
+ android:visible="false" />
<item
android:id="@+id/copy_url"
android:title="@string/copy_original_url"