diff --git a/cheogram.doap b/cheogram.doap index b1d82524d56649e8550dbdf2cdd4261105997dcf..a410ac780f657a1be87494ef3b5a368110bbdd28 100644 --- a/cheogram.doap +++ b/cheogram.doap @@ -60,6 +60,13 @@ + + + + complete + 0.2.1 + + diff --git a/src/cheogram/res/values/strings.xml b/src/cheogram/res/values/strings.xml index 3ea8c47d153ddcf67d8bc2204ab142fc1e442fa7..b5836c46ec07f29f862c3b873adb0beb89230563 100644 --- a/src/cheogram/res/values/strings.xml +++ b/src/cheogram/res/values/strings.xml @@ -31,4 +31,7 @@ Incoming calls from phone numbers may ring with your system dialler instead of this app\'s notification settings Save as Sticker Sticker Name + Moderate + Moderation Resaon + Unable to Moderate diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 37bfb1529fbf56c91c9d29cdef4775de10490048..d3244ae612a10b423265f0b119ba41c4ba1ff88a 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -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; } } } diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 62e2001c9a059e38ab1516562e644a507737c756..8b940a440ec97e39060268e3909f6ab26d05fe8f 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -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; diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index ee8f4adb12c3d2f0de0ccb808e3b0809f80fa10c..bd4cd01750a8084779316f639d763f3f9fde0e75 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -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); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 68fc3e714be4528c692f13304157767132e0ed9a..961e016f96fdae6ce27f23a21b03a681be90be1a 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -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()); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 028b6b8168d0f14147fb56cb6e8ab4de2cd0eb90..a5f827b1b1120dccf381c55edd65562431b38dd3 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -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()); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 50721d9dfeff1defbae6dba280f6b20445ab97c2..30f43b3d4bee3758dfb952872dd09bf0e4e01a30 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -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; diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index fb81d1759e67c9ddc43cb101215a40e7ed9bc7cc..e99408e2507f348f331d9e45717d77c9c2d46d4b 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -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); diff --git a/src/main/res/menu/message_context.xml b/src/main/res/menu/message_context.xml index 693674571af7e1dc7464f6936bc3011c9039410e..b845c444f7c929cd0adaf1d3c10a76288083fe4f 100644 --- a/src/main/res/menu/message_context.xml +++ b/src/main/res/menu/message_context.xml @@ -38,6 +38,10 @@ android:id="@+id/retract_message" android:title="@string/retract_message" android:visible="false" /> +