kopia lustrzana https://github.com/ryukoposting/Signal-Android
Sync mute status via storage service.
rodzic
25ce2a649a
commit
e096ba27ce
|
@ -1283,6 +1283,7 @@ public class RecipientDatabase extends Database {
|
|||
values.put(USERNAME, TextUtils.isEmpty(username) ? null : username);
|
||||
values.put(PROFILE_SHARING, contact.isProfileSharingEnabled() ? "1" : "0");
|
||||
values.put(BLOCKED, contact.isBlocked() ? "1" : "0");
|
||||
values.put(MUTE_UNTIL, contact.getMuteUntil());
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(contact.getId().getRaw()));
|
||||
values.put(DIRTY, DirtyState.CLEAN.getId());
|
||||
|
||||
|
@ -1305,6 +1306,7 @@ public class RecipientDatabase extends Database {
|
|||
values.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId());
|
||||
values.put(PROFILE_SHARING, groupV1.isProfileSharingEnabled() ? "1" : "0");
|
||||
values.put(BLOCKED, groupV1.isBlocked() ? "1" : "0");
|
||||
values.put(MUTE_UNTIL, groupV1.getMuteUntil());
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV1.getId().getRaw()));
|
||||
values.put(DIRTY, DirtyState.CLEAN.getId());
|
||||
|
||||
|
@ -1323,6 +1325,7 @@ public class RecipientDatabase extends Database {
|
|||
values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId());
|
||||
values.put(PROFILE_SHARING, groupV2.isProfileSharingEnabled() ? "1" : "0");
|
||||
values.put(BLOCKED, groupV2.isBlocked() ? "1" : "0");
|
||||
values.put(MUTE_UNTIL, groupV2.getMuteUntil());
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV2.getId().getRaw()));
|
||||
values.put(DIRTY, DirtyState.CLEAN.getId());
|
||||
|
||||
|
@ -1661,7 +1664,9 @@ public class RecipientDatabase extends Database {
|
|||
values.put(MUTE_UNTIL, until);
|
||||
if (update(id, values)) {
|
||||
Recipient.live(id).refresh();
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
}
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
}
|
||||
|
||||
public void setSeenFirstInviteReminder(@NonNull RecipientId id) {
|
||||
|
|
|
@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
|
|||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.migrations.StorageServiceMigrationJob;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.storage.AccountRecordProcessor;
|
||||
|
@ -25,6 +26,7 @@ import org.thoughtcrime.securesms.storage.ContactRecordProcessor;
|
|||
import org.thoughtcrime.securesms.storage.GroupV1RecordProcessor;
|
||||
import org.thoughtcrime.securesms.storage.GroupV2RecordProcessor;
|
||||
import org.thoughtcrime.securesms.storage.StorageRecordProcessor;
|
||||
import org.thoughtcrime.securesms.storage.StorageRecordUpdate;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyDifferenceResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.LocalWriteResult;
|
||||
|
@ -42,6 +44,7 @@ import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
|||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
|
@ -107,6 +110,27 @@ import java.util.concurrent.TimeUnit;
|
|||
* converting local data into a format that can be compared with, merged, and eventually written
|
||||
* back to both local and remote data stores is tiresome. There's also lots of general bookkeeping,
|
||||
* error handling, cleanup scenarios, logging, etc.
|
||||
*
|
||||
* == Syncing a new field on an existing record ==
|
||||
*
|
||||
* - Add the field the the respective proto
|
||||
* - Update the respective model (i.e. {@link SignalContactRecord})
|
||||
* - Add getters
|
||||
* - Update the builder
|
||||
* - Update {@link SignalRecord#describeDiff(SignalRecord)}.
|
||||
* - Update the respective record processor (i.e {@link ContactRecordProcessor}). You need to make
|
||||
* sure that you're:
|
||||
* - Merging the attributes, likely preferring remote
|
||||
* - Adding to doParamsMatch()
|
||||
* - Adding the parameter to the builder chain when creating a merged model
|
||||
* - Update builder usage in StorageSyncModels
|
||||
* - Handle the new data when writing to the local storage
|
||||
* (i.e. {@link RecipientDatabase#applyStorageSyncContactUpdate(StorageRecordUpdate)}).
|
||||
* - Make sure that whenever you change the field in the UI, we mark the row as dirty and call
|
||||
* {@link StorageSyncHelper#scheduleSyncForDataChange()}.
|
||||
* - If you're syncing a field that was otherwise already present in the UI, you'll probably want
|
||||
* to enqueue a {@link StorageServiceMigrationJob} as an app migration to make sure it gets
|
||||
* synced.
|
||||
*/
|
||||
public class StorageSyncJobV2 extends BaseJob {
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ public class ApplicationMigrations {
|
|||
|
||||
private static final int LEGACY_CANONICAL_VERSION = 455;
|
||||
|
||||
public static final int CURRENT_VERSION = 30;
|
||||
public static final int CURRENT_VERSION = 31;
|
||||
|
||||
private static final class Version {
|
||||
static final int LEGACY = 1;
|
||||
|
@ -71,6 +71,8 @@ public class ApplicationMigrations {
|
|||
static final int DAY_BY_DAY_STICKERS = 26;
|
||||
static final int BLOB_LOCATION = 27;
|
||||
static final int SYSTEM_NAME_SPLIT = 28;
|
||||
// Versions 29, 30 accidentally skipped
|
||||
static final int MUTE_SYNC = 31;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -301,6 +303,10 @@ public class ApplicationMigrations {
|
|||
jobs.put(Version.SYSTEM_NAME_SPLIT, new DirectoryRefreshMigrationJob());
|
||||
}
|
||||
|
||||
if (lastSeenVersion < Version.MUTE_SYNC) {
|
||||
jobs.put(Version.MUTE_SYNC, new StorageServiceMigrationJob());
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,9 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob;
|
|||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
/**
|
||||
* Just runs a storage sync. Useful if you've started syncing a new field to storage service.
|
||||
*/
|
||||
public class StorageServiceMigrationJob extends MigrationJob {
|
||||
|
||||
private static final String TAG = Log.tag(StorageServiceMigrationJob.class);
|
||||
|
|
|
@ -97,8 +97,9 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
boolean profileSharing = remote.isProfileSharingEnabled();
|
||||
boolean archived = remote.isArchived();
|
||||
boolean forcedUnread = remote.isForcedUnread();
|
||||
boolean matchesRemote = doParamsMatch(remote, unknownFields, address, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread);
|
||||
boolean matchesLocal = doParamsMatch(local, unknownFields, address, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread);
|
||||
long muteUntil = remote.getMuteUntil();
|
||||
boolean matchesRemote = doParamsMatch(remote, unknownFields, address, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread, muteUntil);
|
||||
boolean matchesLocal = doParamsMatch(local, unknownFields, address, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread, muteUntil);
|
||||
|
||||
if (matchesRemote) {
|
||||
return remote;
|
||||
|
@ -116,6 +117,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
.setBlocked(blocked)
|
||||
.setProfileSharingEnabled(profileSharing)
|
||||
.setForcedUnread(forcedUnread)
|
||||
.setMuteUntil(muteUntil)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
@ -154,7 +156,8 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
boolean blocked,
|
||||
boolean profileSharing,
|
||||
boolean archived,
|
||||
boolean forcedUnread)
|
||||
boolean forcedUnread,
|
||||
long muteUntil)
|
||||
{
|
||||
return Arrays.equals(contact.serializeUnknownFields(), unknownFields) &&
|
||||
Objects.equals(contact.getAddress(), address) &&
|
||||
|
@ -167,6 +170,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
|
|||
contact.isBlocked() == blocked &&
|
||||
contact.isProfileSharingEnabled() == profileSharing &&
|
||||
contact.isArchived() == archived &&
|
||||
contact.isForcedUnread() == forcedUnread;
|
||||
contact.isForcedUnread() == forcedUnread &&
|
||||
contact.getMuteUntil() == muteUntil;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.storage;
|
|||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
|
@ -80,9 +81,10 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
|
|||
boolean profileSharing = remote.isProfileSharingEnabled();
|
||||
boolean archived = remote.isArchived();
|
||||
boolean forcedUnread = remote.isForcedUnread();
|
||||
long muteUntil = remote.getMuteUntil();
|
||||
|
||||
boolean matchesRemote = Arrays.equals(unknownFields, remote.serializeUnknownFields()) && blocked == remote.isBlocked() && profileSharing == remote.isProfileSharingEnabled() && archived == remote.isArchived() && forcedUnread == remote.isForcedUnread();
|
||||
boolean matchesLocal = Arrays.equals(unknownFields, local.serializeUnknownFields()) && blocked == local.isBlocked() && profileSharing == local.isProfileSharingEnabled() && archived == local.isArchived() && forcedUnread == local.isForcedUnread();
|
||||
boolean matchesRemote = doParamsMatch(remote, unknownFields, blocked, profileSharing, archived, forcedUnread, muteUntil);
|
||||
boolean matchesLocal = doParamsMatch(local, unknownFields, blocked, profileSharing, archived, forcedUnread, muteUntil);
|
||||
|
||||
if (matchesRemote) {
|
||||
return remote;
|
||||
|
@ -94,6 +96,7 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
|
|||
.setBlocked(blocked)
|
||||
.setProfileSharingEnabled(blocked)
|
||||
.setForcedUnread(forcedUnread)
|
||||
.setMuteUntil(muteUntil)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
@ -117,4 +120,20 @@ public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<
|
|||
return lhs.getGroupId()[0] - rhs.getGroupId()[0];
|
||||
}
|
||||
}
|
||||
|
||||
private boolean doParamsMatch(@NonNull SignalGroupV1Record group,
|
||||
@Nullable byte[] unknownFields,
|
||||
boolean blocked,
|
||||
boolean profileSharing,
|
||||
boolean archived,
|
||||
boolean forcedUnread,
|
||||
long muteUntil)
|
||||
{
|
||||
return Arrays.equals(unknownFields, group.serializeUnknownFields()) &&
|
||||
blocked == group.isBlocked() &&
|
||||
profileSharing == group.isProfileSharingEnabled() &&
|
||||
archived == group.isArchived() &&
|
||||
forcedUnread == group.isForcedUnread() &&
|
||||
muteUntil == group.getMuteUntil();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.storage;
|
|||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
|
@ -15,6 +16,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.thoughtcrime.securesms.util.ParcelUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
@ -61,9 +63,10 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
|
|||
boolean profileSharing = remote.isProfileSharingEnabled();
|
||||
boolean archived = remote.isArchived();
|
||||
boolean forcedUnread = remote.isForcedUnread();
|
||||
long muteUntil = remote.getMuteUntil();
|
||||
|
||||
boolean matchesRemote = Arrays.equals(unknownFields, remote.serializeUnknownFields()) && blocked == remote.isBlocked() && profileSharing == remote.isProfileSharingEnabled() && archived == remote.isArchived() && forcedUnread == remote.isForcedUnread();
|
||||
boolean matchesLocal = Arrays.equals(unknownFields, local.serializeUnknownFields()) && blocked == local.isBlocked() && profileSharing == local.isProfileSharingEnabled() && archived == local.isArchived() && forcedUnread == local.isForcedUnread();
|
||||
boolean matchesRemote = doParamsMatch(remote, unknownFields, blocked, profileSharing, archived, forcedUnread, muteUntil);
|
||||
boolean matchesLocal = doParamsMatch(local, unknownFields, blocked, profileSharing, archived, forcedUnread, muteUntil);
|
||||
|
||||
if (matchesRemote) {
|
||||
return remote;
|
||||
|
@ -76,6 +79,7 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
|
|||
.setProfileSharingEnabled(blocked)
|
||||
.setArchived(archived)
|
||||
.setForcedUnread(forcedUnread)
|
||||
.setMuteUntil(muteUntil)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
@ -116,4 +120,20 @@ public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<
|
|||
return lhs.getMasterKeyBytes()[0] - rhs.getMasterKeyBytes()[0];
|
||||
}
|
||||
}
|
||||
|
||||
private boolean doParamsMatch(@NonNull SignalGroupV2Record group,
|
||||
@Nullable byte[] unknownFields,
|
||||
boolean blocked,
|
||||
boolean profileSharing,
|
||||
boolean archived,
|
||||
boolean forcedUnread,
|
||||
long muteUntil)
|
||||
{
|
||||
return Arrays.equals(unknownFields, group.serializeUnknownFields()) &&
|
||||
blocked == group.isBlocked() &&
|
||||
profileSharing == group.isProfileSharingEnabled() &&
|
||||
archived == group.isArchived() &&
|
||||
forcedUnread == group.isForcedUnread() &&
|
||||
muteUntil == group.getMuteUntil();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -94,6 +94,7 @@ public final class StorageSyncModels {
|
|||
.setIdentityState(localToRemoteIdentityState(recipient.getSyncExtras().getIdentityStatus()))
|
||||
.setArchived(recipient.getSyncExtras().isArchived())
|
||||
.setForcedUnread(recipient.getSyncExtras().isForcedUnread())
|
||||
.setMuteUntil(recipient.getMuteUntil())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -114,6 +115,7 @@ public final class StorageSyncModels {
|
|||
.setProfileSharingEnabled(recipient.isProfileSharing())
|
||||
.setArchived(recipient.getSyncExtras().isArchived())
|
||||
.setForcedUnread(recipient.getSyncExtras().isForcedUnread())
|
||||
.setMuteUntil(recipient.getMuteUntil())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -140,6 +142,7 @@ public final class StorageSyncModels {
|
|||
.setProfileSharingEnabled(recipient.isProfileSharing())
|
||||
.setArchived(recipient.getSyncExtras().isArchived())
|
||||
.setForcedUnread(recipient.getSyncExtras().isForcedUnread())
|
||||
.setMuteUntil(recipient.getMuteUntil())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
|
|
@ -104,6 +104,10 @@ public final class SignalContactRecord implements SignalRecord {
|
|||
diff.add("ForcedUnread");
|
||||
}
|
||||
|
||||
if (!Objects.equals(this.getMuteUntil(), that.getMuteUntil())) {
|
||||
diff.add("MuteUntil");
|
||||
}
|
||||
|
||||
if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) {
|
||||
diff.add("UnknownFields");
|
||||
}
|
||||
|
@ -166,6 +170,10 @@ public final class SignalContactRecord implements SignalRecord {
|
|||
return proto.getMarkedUnread();
|
||||
}
|
||||
|
||||
public long getMuteUntil() {
|
||||
return proto.getMutedUntilTimestamp();
|
||||
}
|
||||
|
||||
ContactRecord toProto() {
|
||||
return proto;
|
||||
}
|
||||
|
@ -253,6 +261,11 @@ public final class SignalContactRecord implements SignalRecord {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder setMuteUntil(long muteUntil) {
|
||||
builder.setMutedUntilTimestamp(muteUntil);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SignalContactRecord build() {
|
||||
ContactRecord proto = builder.build();
|
||||
|
||||
|
|
|
@ -60,6 +60,10 @@ public final class SignalGroupV1Record implements SignalRecord {
|
|||
diff.add("ForcedUnread");
|
||||
}
|
||||
|
||||
if (!Objects.equals(this.getMuteUntil(), that.getMuteUntil())) {
|
||||
diff.add("MuteUntil");
|
||||
}
|
||||
|
||||
if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) {
|
||||
diff.add("UnknownFields");
|
||||
}
|
||||
|
@ -98,6 +102,10 @@ public final class SignalGroupV1Record implements SignalRecord {
|
|||
return proto.getMarkedUnread();
|
||||
}
|
||||
|
||||
public long getMuteUntil() {
|
||||
return proto.getMutedUntilTimestamp();
|
||||
}
|
||||
|
||||
GroupV1Record toProto() {
|
||||
return proto;
|
||||
}
|
||||
|
@ -154,6 +162,11 @@ public final class SignalGroupV1Record implements SignalRecord {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder setMuteUntil(long muteUntil) {
|
||||
builder.setMutedUntilTimestamp(muteUntil);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SignalGroupV1Record build() {
|
||||
GroupV1Record proto = builder.build();
|
||||
|
||||
|
|
|
@ -62,6 +62,10 @@ public final class SignalGroupV2Record implements SignalRecord {
|
|||
diff.add("ForcedUnread");
|
||||
}
|
||||
|
||||
if (!Objects.equals(this.getMuteUntil(), that.getMuteUntil())) {
|
||||
diff.add("MuteUntil");
|
||||
}
|
||||
|
||||
if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) {
|
||||
diff.add("UnknownFields");
|
||||
}
|
||||
|
@ -108,6 +112,11 @@ public final class SignalGroupV2Record implements SignalRecord {
|
|||
return proto.getMarkedUnread();
|
||||
}
|
||||
|
||||
public long getMuteUntil() {
|
||||
return proto.getMutedUntilTimestamp();
|
||||
}
|
||||
|
||||
|
||||
GroupV2Record toProto() {
|
||||
return proto;
|
||||
}
|
||||
|
@ -168,6 +177,11 @@ public final class SignalGroupV2Record implements SignalRecord {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder setMuteUntil(long muteUntil) {
|
||||
builder.setMutedUntilTimestamp(muteUntil);
|
||||
return this;
|
||||
}
|
||||
|
||||
public SignalGroupV2Record build() {
|
||||
GroupV2Record proto = builder.build();
|
||||
|
||||
|
|
|
@ -69,34 +69,37 @@ message ContactRecord {
|
|||
UNVERIFIED = 2;
|
||||
}
|
||||
|
||||
string serviceUuid = 1;
|
||||
string serviceE164 = 2;
|
||||
bytes profileKey = 3;
|
||||
bytes identityKey = 4;
|
||||
IdentityState identityState = 5;
|
||||
string givenName = 6;
|
||||
string familyName = 7;
|
||||
string username = 8;
|
||||
bool blocked = 9;
|
||||
bool whitelisted = 10;
|
||||
bool archived = 11;
|
||||
bool markedUnread = 12;
|
||||
string serviceUuid = 1;
|
||||
string serviceE164 = 2;
|
||||
bytes profileKey = 3;
|
||||
bytes identityKey = 4;
|
||||
IdentityState identityState = 5;
|
||||
string givenName = 6;
|
||||
string familyName = 7;
|
||||
string username = 8;
|
||||
bool blocked = 9;
|
||||
bool whitelisted = 10;
|
||||
bool archived = 11;
|
||||
bool markedUnread = 12;
|
||||
uint64 mutedUntilTimestamp = 13;
|
||||
}
|
||||
|
||||
message GroupV1Record {
|
||||
bytes id = 1;
|
||||
bool blocked = 2;
|
||||
bool whitelisted = 3;
|
||||
bool archived = 4;
|
||||
bool markedUnread = 5;
|
||||
bytes id = 1;
|
||||
bool blocked = 2;
|
||||
bool whitelisted = 3;
|
||||
bool archived = 4;
|
||||
bool markedUnread = 5;
|
||||
uint64 mutedUntilTimestamp = 6;
|
||||
}
|
||||
|
||||
message GroupV2Record {
|
||||
bytes masterKey = 1;
|
||||
bool blocked = 2;
|
||||
bool whitelisted = 3;
|
||||
bool archived = 4;
|
||||
bool markedUnread = 5;
|
||||
bytes masterKey = 1;
|
||||
bool blocked = 2;
|
||||
bool whitelisted = 3;
|
||||
bool archived = 4;
|
||||
bool markedUnread = 5;
|
||||
uint64 mutedUntilTimestamp = 6;
|
||||
}
|
||||
|
||||
message Payments {
|
||||
|
|
Ładowanie…
Reference in New Issue