Detailed changes
@@ -653,7 +653,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
}
}
- public void populateWithMessages(final List<Message> messages) {
+ public void populateWithMessages(final List<Message> messages, XmppConnectionService xmppConnectionService) {
synchronized (this.messages) {
messages.clear();
messages.addAll(this.messages);
@@ -676,7 +676,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
thread.first = m;
}
}
- if (m.wasMergedIntoPrevious() || (m.getSubject() != null && !m.isOOb() && (m.getRawBody() == null || m.getRawBody().length() == 0)) || (getLockThread() && !extraIds.contains(m.replyId()) && (mthread == null || !mthread.getContent().equals(getThread() == null ? "" : getThread().getContent())))) {
+ if (m.wasMergedIntoPrevious(xmppConnectionService) || (m.getSubject() != null && !m.isOOb() && (m.getRawBody() == null || m.getRawBody().length() == 0)) || (getLockThread() && !extraIds.contains(m.replyId()) && (mthread == null || !mthread.getContent().equals(getThread() == null ? "" : getThread().getContent())))) {
iterator.remove();
} else if (getLockThread() && mthread != null) {
Element reply = m.getReply();
@@ -59,6 +59,7 @@ import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.StringUtils;
import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
@@ -125,6 +126,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
protected String conversationUuid;
protected Jid counterpart;
protected Jid trueCounterpart;
+ protected String occupantId = null;
protected String body;
protected SpannableStringBuilder spannableBody = null;
protected String subject;
@@ -273,7 +275,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
}
- return new Message(conversation,
+ Message m = new Message(conversation,
cursor.getString(cursor.getColumnIndex(UUID)),
cursor.getString(cursor.getColumnIndex(CONVERSATION)),
fromString(cursor.getString(cursor.getColumnIndex(COUNTERPART))),
@@ -301,6 +303,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
cursor.getString(cursor.getColumnIndex("fileParams")),
payloads
);
+ m.setOccupantId(cursor.getString(cursor.getColumnIndex("occupant_id")));
+ return m;
}
private static Jid fromString(String value) {
@@ -344,6 +348,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
}
}
values.put("payloads", payloads.size() < 1 ? null : payloads.stream().map(Object::toString).collect(Collectors.joining()));
+ values.put("occupant_id", occupantId);
return values;
}
@@ -640,8 +645,17 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
addPayload(thread);
}
+ public void setOccupantId(final String id) {
+ occupantId = id;
+ }
+
+ public String getOccupantId() {
+ return occupantId;
+ }
+
public void setMucUser(MucOptions.User user) {
this.user = new WeakReference<>(user);
+ if (user != null && user.getOccupantId() != null) setOccupantId(user.getOccupantId());
}
public boolean sameMucUser(Message otherMessage) {
@@ -1096,9 +1110,13 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
return time;
}
- public boolean wasMergedIntoPrevious() {
+ public boolean wasMergedIntoPrevious(XmppConnectionService xmppConnectionService) {
Message prev = this.prev();
if (prev != null && getModerated() != null && prev.getModerated() != null) return true;
+ if (getOccupantId() != null && xmppConnectionService != null) {
+ final boolean muted = getStatus() == Message.STATUS_RECEIVED && conversation.getMode() == Conversation.MODE_MULTI && xmppConnectionService.isMucUserMuted(new MucOptions.User(null, conversation.getJid(), getOccupantId(), null, null));
+ if (prev != null && muted && getOccupantId().equals(prev.getOccupantId())) return true;
+ }
return prev != null && prev.mergeable(this);
}
@@ -56,7 +56,7 @@ public class MucOptions {
this.account = conversation.getAccount();
this.conversation = conversation;
final String nick = getProposedNick(conversation.getAttribute("mucNick"));
- this.self = new User(this, createJoinJid(nick), nick, new HashSet<>());
+ this.self = new User(this, createJoinJid(nick), null, nick, new HashSet<>());
this.self.affiliation = Affiliation.of(conversation.getAttribute("affiliation"));
this.self.role = Role.of(conversation.getAttribute("role"));
}
@@ -342,10 +342,24 @@ public class MucOptions {
return null;
}
+ public User findUserByOccupantId(final String id) {
+ if (id == null) {
+ return null;
+ }
+ synchronized (users) {
+ for (User user : users) {
+ if (id.equals(user.getOccupantId())) {
+ return user;
+ }
+ }
+ }
+ return null;
+ }
+
public User findOrCreateUserByRealJid(Jid jid, Jid fullJid) {
User user = findUserByRealJid(jid);
if (user == null) {
- user = new User(this, fullJid, null, new HashSet<>());
+ user = new User(this, fullJid, null, null, new HashSet<>());
user.setRealJid(jid);
}
return user;
@@ -544,7 +558,7 @@ public class MucOptions {
private List<User> getFallbackUsersFromCryptoTargets() {
List<User> users = new ArrayList<>();
for (Jid jid : conversation.getAcceptedCryptoTargets()) {
- User user = new User(this, null, null, new HashSet<>());
+ User user = new User(this, null, null, null, new HashSet<>());
user.setRealJid(jid);
users.add(user);
}
@@ -832,10 +846,12 @@ public class MucOptions {
private final MucOptions options;
private ChatState chatState = Config.DEFAULT_CHAT_STATE;
protected Set<Hat> hats;
+ protected String occupantId;
- public User(MucOptions options, Jid fullJid, final String nick, final Set<Hat> hats) {
+ public User(MucOptions options, Jid fullJid, final String occupantId, final String nick, final Set<Hat> hats) {
this.options = options;
this.fullJid = fullJid;
+ this.occupantId = occupantId;
this.nick = nick;
this.hats = hats;
}
@@ -844,6 +860,14 @@ public class MucOptions {
return fullJid == null ? null : fullJid.getResource();
}
+ public Jid getMuc() {
+ return fullJid == null ? null : fullJid.asBareJid();
+ }
+
+ public String getOccupantId() {
+ return occupantId;
+ }
+
public String getNick() {
return nick == null ? getName() : nick;
}
@@ -134,10 +134,10 @@ public abstract class AbstractParser {
}
public static MucOptions.User parseItem(Conversation conference, Element item) {
- return parseItem(conference,item,null,null,new Element("hats", "urn:xmpp:hats:0"));
+ return parseItem(conference,item,null,null,null,new Element("hats", "urn:xmpp:hats:0"));
}
- public static MucOptions.User parseItem(Conversation conference, Element item, Jid fullJid, final String nicknameIn, final Element hatsEl) {
+ public static MucOptions.User parseItem(Conversation conference, Element item, Jid fullJid, final Element occupantId, final String nicknameIn, final Element hatsEl) {
final String local = conference.getJid().getLocal();
final String domain = conference.getJid().getDomain().toEscapedString();
String affiliation = item.getAttribute("affiliation");
@@ -165,7 +165,7 @@ public abstract class AbstractParser {
hats.add(new MucOptions.Hat(hat));
}
}
- MucOptions.User user = new MucOptions.User(conference.getMucOptions(), fullJid, nickname, hatsEl == null ? null : hats);
+ MucOptions.User user = new MucOptions.User(conference.getMucOptions(), fullJid, occupantId == null ? null : occupantId.getAttribute("id"), nickname, hatsEl == null ? null : hats);
if (InvalidJid.isValid(realJid)) {
user.setRealJid(realJid);
}
@@ -728,6 +728,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
if (conversationMultiMode) {
message.setMucUser(conversation.getMucOptions().findUserByFullJid(counterpart));
+ final Element occupantId = packet.findChild("occupant-id", "urn:xmpp:occupant-id:0");
+ if (occupantId != null) message.setOccupantId(occupantId.getAttribute("id"));
final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
Jid trueCounterpart;
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
@@ -69,6 +69,7 @@ public class PresenceParser extends AbstractParser implements
hats = packet.findChild("hats", "xmpp:prosody.im/protocol/hats:1");
}
if (hats == null) hats = new Element("hats", "urn:xmpp:hats:0");
+ final Element occupantId = packet.findChild("occupant-id", "urn:xmpp:occupant-id:0");
Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
final List<String> codes = getStatusCodes(x);
if (type == null) {
@@ -76,7 +77,7 @@ public class PresenceParser extends AbstractParser implements
Element item = x.findChild("item");
if (item != null && !from.isBareJid()) {
mucOptions.setError(MucOptions.Error.NONE);
- MucOptions.User user = parseItem(conversation, item, from, nick == null ? null : nick.getContent(), hats);
+ MucOptions.User user = parseItem(conversation, item, from, occupantId, nick == null ? null : nick.getContent(), hats);
if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) || (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && jid.equals(InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"))))) {
if (mucOptions.setOnline()) {
mXmppConnectionService.getAvatarService().clear(mucOptions);
@@ -180,7 +181,7 @@ public class PresenceParser extends AbstractParser implements
} else if (!from.isBareJid()){
Element item = x.findChild("item");
if (item != null) {
- mucOptions.updateUser(parseItem(conversation, item, from, nick == null ? null : nick.getContent(), hats));
+ mucOptions.updateUser(parseItem(conversation, item, from, occupantId, nick == null ? null : nick.getContent(), hats));
}
MucOptions.User user = mucOptions.deleteUser(from);
if (user != null) {
@@ -14,6 +14,8 @@ import android.util.Log;
import com.cheogram.android.WebxdcUpdate;
import com.google.common.base.Stopwatch;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.HashMultimap;
import org.json.JSONException;
import org.json.JSONObject;
@@ -52,6 +54,7 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.PresenceTemplate;
import eu.siacs.conversations.entities.Roster;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
@@ -320,6 +323,22 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("PRAGMA cheogram.user_version = 9");
}
+ if(cheogramVersion < 10) {
+ db.execSQL(
+ "CREATE TABLE cheogram.muted_participants (" +
+ "muc_jid TEXT NOT NULL, " +
+ "occupant_id TEXT NOT NULL, " +
+ "nick TEXT NOT NULL," +
+ "PRIMARY KEY (muc_jid, occupant_id)" +
+ ")"
+ );
+ db.execSQL(
+ "ALTER TABLE cheogram." + Message.TABLENAME + " " +
+ "ADD COLUMN occupant_id TEXT"
+ );
+ db.execSQL("PRAGMA cheogram.user_version = 10");
+ }
+
db.setTransactionSuccessful();
} finally {
db.endTransaction();
@@ -863,6 +882,41 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("DELETE FROM cheogram.blocked_media");
}
+ public Multimap<String, String> loadMutedMucUsers() {
+ Multimap<String, String> result = HashMultimap.create();
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor cursor = db.query("cheogram.muted_participants", new String[]{"muc_jid", "occupant_id"}, null, null, null, null, null);
+ while (cursor.moveToNext()) {
+ result.put(cursor.getString(0), cursor.getString(1));
+ }
+ cursor.close();
+ return result;
+ }
+
+ public boolean muteMucUser(MucOptions.User user) {
+ if (user.getMuc() == null || user.getOccupantId() == null) return false;
+
+ SQLiteDatabase db = this.getWritableDatabase();
+ ContentValues cv = new ContentValues();
+ cv.put("muc_jid", user.getMuc().toString());
+ cv.put("occupant_id", user.getOccupantId());
+ cv.put("nick", user.getNick());
+ db.insertWithOnConflict("cheogram.muted_participants", null, cv, SQLiteDatabase.CONFLICT_REPLACE);
+
+ return true;
+ }
+
+ public boolean unmuteMucUser(MucOptions.User user) {
+ if (user.getMuc() == null || user.getOccupantId() == null) return false;
+
+ SQLiteDatabase db = this.getWritableDatabase();
+ String where = "muc_jid=? AND occupant_id=?";
+ String[] whereArgs = {user.getMuc().toString(), user.getOccupantId()};
+ db.delete("cheogram.muted_participants", where, whereArgs);
+
+ return true;
+ }
+
public void insertWebxdcUpdate(final WebxdcUpdate update) {
SQLiteDatabase db = this.getWritableDatabase();
db.insertWithOnConflict("cheogram.webxdc_updates", null, update.getContentValues(), SQLiteDatabase.CONFLICT_IGNORE);
@@ -65,6 +65,7 @@ import com.cheogram.android.WebxdcUpdate;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
+import com.google.common.collect.Multimap;
import com.google.common.io.Files;
import com.kedia.ogparser.JsoupProxy;
@@ -258,6 +259,7 @@ public class XmppConnectionService extends Service {
}
};
public DatabaseBackend databaseBackend;
+ private Multimap<String, String> mutedMucUsers;
private final ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor("ContactMerger");
private final ReplacingSerialSingleThreadExecutor mStickerScanExecutor = new ReplacingSerialSingleThreadExecutor("StickerScan");
private long mLastActivity = 0;
@@ -621,6 +623,24 @@ public class XmppConnectionService extends Service {
this.databaseBackend.saveCid(cid, file, url);
}
+ public boolean muteMucUser(MucOptions.User user) {
+ boolean muted = databaseBackend.muteMucUser(user);
+ if (!muted) return false;
+ mutedMucUsers.put(user.getMuc().toString(), user.getOccupantId());
+ return true;
+ }
+
+ public boolean unmuteMucUser(MucOptions.User user) {
+ boolean unmuted = databaseBackend.unmuteMucUser(user);
+ if (!unmuted) return false;
+ mutedMucUsers.remove(user.getMuc().toString(), user.getOccupantId());
+ return true;
+ }
+
+ public boolean isMucUserMuted(MucOptions.User user) {
+ return mutedMucUsers.containsEntry("" + user.getMuc(), user.getOccupantId());
+ }
+
public void blockMedia(File f) {
try {
Cid[] cids = getFileBackend().calculateCids(new FileInputStream(f));
@@ -2408,6 +2428,7 @@ public class XmppConnectionService extends Service {
if (DatabaseBackend.requiresMessageIndexRebuild()) {
DatabaseBackend.getInstance(this).rebuildMessagesIndex();
}
+ mutedMucUsers = databaseBackend.loadMutedMucUsers();
final long deletionDate = getAutomaticMessageDeletionDate();
mLastExpiryRun.set(SystemClock.elapsedRealtime());
if (deletionDate > 0) {
@@ -387,7 +387,7 @@ public class ConversationFragment extends XmppFragment
.populateWithMessages(
ConversationFragment
.this
- .messageList);
+ .messageList, activity.xmppConnectionService);
try {
updateStatusMessages();
} catch (IllegalStateException e) {
@@ -772,7 +772,7 @@ public class ConversationFragment extends XmppFragment
return i;
} else {
Message next = messages.get(i);
- while (next != null && next.wasMergedIntoPrevious()) {
+ while (next != null && next.wasMergedIntoPrevious(activity.xmppConnectionService)) {
if (uuid.equals(next.getUuid())) {
return i;
}
@@ -2657,7 +2657,7 @@ public class ConversationFragment extends XmppFragment
}
}
if (message != null) {
- while (message.next() != null && message.next().wasMergedIntoPrevious()) {
+ while (message.next() != null && message.next().wasMergedIntoPrevious(activity.xmppConnectionService)) {
message = message.next();
}
return message;
@@ -3567,7 +3567,7 @@ public class ConversationFragment extends XmppFragment
if (messageListAdapter.hasSelection()) {
if (notifyConversationRead) binding.messagesView.postDelayed(this::refresh, 1000L);
} else {
- conversation.populateWithMessages(this.messageList);
+ conversation.populateWithMessages(this.messageList, activity.xmppConnectionService);
updateStatusMessages();
this.messageListAdapter.notifyDataSetChanged();
}
@@ -4293,10 +4293,15 @@ public class ConversationFragment extends XmppFragment
tcp != null
? conversation.getMucOptions().findOrCreateUserByRealJid(tcp, cp)
: null;
+ final String occupantId = message.getOccupantId();
+ final User userByOccupantId =
+ occupantId != null
+ ? conversation.getMucOptions().findUserByOccupantId(occupantId)
+ : null;
final User user =
userByRealJid != null
? userByRealJid
- : conversation.getMucOptions().findUserByFullJid(cp);
+ : (userByOccupantId != null ? userByOccupantId : conversation.getMucOptions().findUserByFullJid(cp));
if (user == null) return;
popupMenu.inflate(R.menu.muc_details_context);
final Menu menu = popupMenu.getMenu();
@@ -79,6 +79,7 @@ import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message.FileParams;
import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.Roster;
import eu.siacs.conversations.entities.RtpSessionStatus;
import eu.siacs.conversations.entities.Transferable;
@@ -1081,7 +1082,12 @@ public class MessageAdapter extends ArrayAdapter<Message> {
final Transferable transferable = message.getTransferable();
final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
- if (unInitiatedButKnownSize || message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) {
+
+ final boolean muted = message.getStatus() == Message.STATUS_RECEIVED && conversation.getMode() == Conversation.MODE_MULTI && activity.xmppConnectionService.isMucUserMuted(new MucOptions.User(null, conversation.getJid(), message.getOccupantId(), null, null));
+ if (muted) {
+ // Muted MUC participant
+ displayInfoMessage(viewHolder, "Muted", darkBackground);
+ } else if (unInitiatedButKnownSize || message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) {
if (unInitiatedButKnownSize || (message.isDeleted() && message.getModerated() == null) || transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) {
displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), darkBackground, type);
} else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) {
@@ -1152,7 +1158,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.status_line.setLayoutParams(statusParams);
if (type == RECEIVED) {
- if (commands != null && conversation instanceof Conversation) {
+ if (!muted && commands != null && conversation instanceof Conversation) {
CommandButtonAdapter adapter = new CommandButtonAdapter(activity);
adapter.addAll(commands);
viewHolder.commands_list.setAdapter(adapter);
@@ -1194,7 +1200,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
if (subject == null && message.getThread() != null) {
subject = ((Conversation) message.getConversation()).getThread(message.getThread().getContent()).getSubject();
}
- if (subject == null) {
+ if (muted || subject == null) {
viewHolder.subject.setVisibility(View.GONE);
} else {
viewHolder.subject.setVisibility(View.VISIBLE);
@@ -10,6 +10,7 @@ import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
+import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.databinding.DataBindingUtil;
@@ -102,7 +103,7 @@ public final class MucDetailsContextMenuHelper {
return new Pair<>(items.toArray(new CharSequence[items.size()]), actions.toArray(new Integer[actions.size()]));
}
- public static void configureMucDetailsContextMenu(Activity activity, Menu menu, Conversation conversation, User user) {
+ public static void configureMucDetailsContextMenu(XmppActivity activity, Menu menu, Conversation conversation, User user) {
final MucOptions mucOptions = conversation.getMucOptions();
final boolean advancedMode = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean("advanced_muc_mode", false);
final boolean isGroupChat = mucOptions.isPrivateAndNonAnonymous();
@@ -113,6 +114,16 @@ public final class MucDetailsContextMenuHelper {
blockAvatar.setVisible(true);
}
+ MenuItem muteParticipant = menu.findItem(R.id.action_mute_participant);
+ MenuItem unmuteParticipant = menu.findItem(R.id.action_unmute_participant);
+ if (user != null && user.getOccupantId() != null) {
+ if (activity.xmppConnectionService.isMucUserMuted(user)) {
+ unmuteParticipant.setVisible(true);
+ } else {
+ muteParticipant.setVisible(true);
+ }
+ }
+
if (user != null && user.getRealJid() != null) {
MenuItem showContactDetails = menu.findItem(R.id.action_contact_details);
MenuItem startConversation = menu.findItem(R.id.start_conversation);
@@ -210,6 +221,20 @@ public final class MucDetailsContextMenuHelper {
})
.setNegativeButton(R.string.no, null).show();
return true;
+ case R.id.action_mute_participant:
+ if (activity.xmppConnectionService.muteMucUser(user)) {
+ activity.xmppConnectionService.updateConversationUi();
+ } else {
+ Toast.makeText(activity, "Failed to mute", Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ case R.id.action_unmute_participant:
+ if (activity.xmppConnectionService.unmuteMucUser(user)) {
+ activity.xmppConnectionService.updateConversationUi();
+ } else {
+ Toast.makeText(activity, "Failed to unmute", Toast.LENGTH_SHORT).show();
+ }
+ return true;
case R.id.start_conversation:
startConversation(user, activity);
return true;
@@ -12,6 +12,14 @@
android:id="@+id/action_block_avatar"
android:title="Block Avatar"
android:visible="false" />
+ <item
+ android:id="@+id/action_mute_participant"
+ android:title="Mute Locally"
+ android:visible="false" />
+ <item
+ android:id="@+id/action_unmute_participant"
+ android:title="Unmute Locally"
+ android:visible="false" />
<item
android:id="@+id/invite"
android:title="@string/invite_again"