mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-16 04:39:54 +01:00
Compare commits
1 Commits
load-forum
...
remove-dep
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6eaa54783 |
@@ -54,38 +54,6 @@ public interface CryptoComponent {
|
|||||||
KeyPair ourKeyPair, byte[]... inputs)
|
KeyPair ourKeyPair, byte[]... inputs)
|
||||||
throws GeneralSecurityException;
|
throws GeneralSecurityException;
|
||||||
|
|
||||||
/**
|
|
||||||
* Derives a shared secret from two static and two ephemeral key pairs.
|
|
||||||
* <p>
|
|
||||||
* Do not use this method for new protocols. The shared secret can be
|
|
||||||
* re-derived using the ephemeral public keys and both static private
|
|
||||||
* keys, so keys derived from the shared secret should not be used if
|
|
||||||
* forward secrecy is required. Use {@link #deriveSharedSecret(String,
|
|
||||||
* PublicKey, PublicKey, KeyPair, KeyPair, boolean, byte[]...)} instead.
|
|
||||||
* <p>
|
|
||||||
* TODO: Remove this after a reasonable migration period (added 2023-03-10).
|
|
||||||
* <p>
|
|
||||||
*
|
|
||||||
* @param label A namespaced label indicating the purpose of this shared
|
|
||||||
* secret, to prevent it from being repurposed or colliding with a shared
|
|
||||||
* secret derived for another purpose
|
|
||||||
* @param theirStaticPublicKey The static public key of the remote party
|
|
||||||
* @param theirEphemeralPublicKey The ephemeral public key of the remote
|
|
||||||
* party
|
|
||||||
* @param ourStaticKeyPair The static key pair of the local party
|
|
||||||
* @param ourEphemeralKeyPair The ephemeral key pair of the local party
|
|
||||||
* @param alice True if the local party is Alice
|
|
||||||
* @param inputs Additional inputs that will be included in the
|
|
||||||
* derivation of the shared secret
|
|
||||||
* @return The shared secret
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
SecretKey deriveSharedSecretBadly(String label,
|
|
||||||
PublicKey theirStaticPublicKey, PublicKey theirEphemeralPublicKey,
|
|
||||||
KeyPair ourStaticKeyPair, KeyPair ourEphemeralKeyPair,
|
|
||||||
boolean alice, byte[]... inputs)
|
|
||||||
throws GeneralSecurityException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derives a shared secret from two static and two ephemeral key pairs.
|
* Derives a shared secret from two static and two ephemeral key pairs.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -14,16 +14,6 @@ interface HandshakeConstants {
|
|||||||
*/
|
*/
|
||||||
byte PROTOCOL_MINOR_VERSION = 1;
|
byte PROTOCOL_MINOR_VERSION = 1;
|
||||||
|
|
||||||
/**
|
|
||||||
* Label for deriving the master key when using the deprecated v0.0 key
|
|
||||||
* derivation method.
|
|
||||||
* <p>
|
|
||||||
* TODO: Remove this after a reasonable migration period (added 2023-03-10).
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
String MASTER_KEY_LABEL_0_0 =
|
|
||||||
"org.briarproject.bramble.handshake/MASTER_KEY";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Label for deriving the master key when using the v0.1 key derivation
|
* Label for deriving the master key when using the v0.1 key derivation
|
||||||
* method.
|
* method.
|
||||||
|
|||||||
@@ -12,20 +12,6 @@ interface HandshakeCrypto {
|
|||||||
|
|
||||||
KeyPair generateEphemeralKeyPair();
|
KeyPair generateEphemeralKeyPair();
|
||||||
|
|
||||||
/**
|
|
||||||
* Derives the master key from the given static and ephemeral keys using
|
|
||||||
* the deprecated v0.0 key derivation method.
|
|
||||||
* <p>
|
|
||||||
* TODO: Remove this after a reasonable migration period (added 2023-03-10).
|
|
||||||
*
|
|
||||||
* @param alice Whether the local peer is Alice
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
SecretKey deriveMasterKey_0_0(PublicKey theirStaticPublicKey,
|
|
||||||
PublicKey theirEphemeralPublicKey, KeyPair ourStaticKeyPair,
|
|
||||||
KeyPair ourEphemeralKeyPair, boolean alice)
|
|
||||||
throws GeneralSecurityException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derives the master key from the given static and ephemeral keys using
|
* Derives the master key from the given static and ephemeral keys using
|
||||||
* the v0.1 key derivation method.
|
* the v0.1 key derivation method.
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import javax.inject.Inject;
|
|||||||
|
|
||||||
import static org.briarproject.bramble.contact.HandshakeConstants.ALICE_PROOF_LABEL;
|
import static org.briarproject.bramble.contact.HandshakeConstants.ALICE_PROOF_LABEL;
|
||||||
import static org.briarproject.bramble.contact.HandshakeConstants.BOB_PROOF_LABEL;
|
import static org.briarproject.bramble.contact.HandshakeConstants.BOB_PROOF_LABEL;
|
||||||
import static org.briarproject.bramble.contact.HandshakeConstants.MASTER_KEY_LABEL_0_0;
|
|
||||||
import static org.briarproject.bramble.contact.HandshakeConstants.MASTER_KEY_LABEL_0_1;
|
import static org.briarproject.bramble.contact.HandshakeConstants.MASTER_KEY_LABEL_0_1;
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -32,27 +31,6 @@ class HandshakeCryptoImpl implements HandshakeCrypto {
|
|||||||
return crypto.generateAgreementKeyPair();
|
return crypto.generateAgreementKeyPair();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@Deprecated
|
|
||||||
public SecretKey deriveMasterKey_0_0(PublicKey theirStaticPublicKey,
|
|
||||||
PublicKey theirEphemeralPublicKey, KeyPair ourStaticKeyPair,
|
|
||||||
KeyPair ourEphemeralKeyPair, boolean alice) throws
|
|
||||||
GeneralSecurityException {
|
|
||||||
byte[] theirStatic = theirStaticPublicKey.getEncoded();
|
|
||||||
byte[] theirEphemeral = theirEphemeralPublicKey.getEncoded();
|
|
||||||
byte[] ourStatic = ourStaticKeyPair.getPublic().getEncoded();
|
|
||||||
byte[] ourEphemeral = ourEphemeralKeyPair.getPublic().getEncoded();
|
|
||||||
byte[][] inputs = {
|
|
||||||
alice ? ourStatic : theirStatic,
|
|
||||||
alice ? theirStatic : ourStatic,
|
|
||||||
alice ? ourEphemeral : theirEphemeral,
|
|
||||||
alice ? theirEphemeral : ourEphemeral
|
|
||||||
};
|
|
||||||
return crypto.deriveSharedSecretBadly(MASTER_KEY_LABEL_0_0,
|
|
||||||
theirStaticPublicKey, theirEphemeralPublicKey,
|
|
||||||
ourStaticKeyPair, ourEphemeralKeyPair, alice, inputs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SecretKey deriveMasterKey_0_1(PublicKey theirStaticPublicKey,
|
public SecretKey deriveMasterKey_0_1(PublicKey theirStaticPublicKey,
|
||||||
PublicKey theirEphemeralPublicKey, KeyPair ourStaticKeyPair,
|
PublicKey theirEphemeralPublicKey, KeyPair ourStaticKeyPair,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.briarproject.bramble.contact;
|
|||||||
|
|
||||||
import org.briarproject.bramble.api.FormatException;
|
import org.briarproject.bramble.api.FormatException;
|
||||||
import org.briarproject.bramble.api.Pair;
|
import org.briarproject.bramble.api.Pair;
|
||||||
|
import org.briarproject.bramble.api.UnsupportedVersionException;
|
||||||
import org.briarproject.bramble.api.contact.ContactManager;
|
import org.briarproject.bramble.api.contact.ContactManager;
|
||||||
import org.briarproject.bramble.api.contact.HandshakeManager;
|
import org.briarproject.bramble.api.contact.HandshakeManager;
|
||||||
import org.briarproject.bramble.api.contact.PendingContact;
|
import org.briarproject.bramble.api.contact.PendingContact;
|
||||||
@@ -111,21 +112,12 @@ class HandshakeManagerImpl implements HandshakeManager {
|
|||||||
sendMinorVersion(recordWriter);
|
sendMinorVersion(recordWriter);
|
||||||
sendPublicKey(recordWriter, ourEphemeralKeyPair.getPublic());
|
sendPublicKey(recordWriter, ourEphemeralKeyPair.getPublic());
|
||||||
}
|
}
|
||||||
byte theirMinorVersion = theirMinorVersionAndKey.getFirst();
|
|
||||||
PublicKey theirEphemeralPublicKey = theirMinorVersionAndKey.getSecond();
|
PublicKey theirEphemeralPublicKey = theirMinorVersionAndKey.getSecond();
|
||||||
SecretKey masterKey;
|
SecretKey masterKey;
|
||||||
try {
|
try {
|
||||||
if (theirMinorVersion > 0) {
|
masterKey = handshakeCrypto.deriveMasterKey_0_1(
|
||||||
masterKey = handshakeCrypto.deriveMasterKey_0_1(
|
theirStaticPublicKey, theirEphemeralPublicKey,
|
||||||
theirStaticPublicKey, theirEphemeralPublicKey,
|
ourStaticKeyPair, ourEphemeralKeyPair, alice);
|
||||||
ourStaticKeyPair, ourEphemeralKeyPair, alice);
|
|
||||||
} else {
|
|
||||||
// TODO: Remove this branch after a reasonable migration
|
|
||||||
// period (added 2023-03-10).
|
|
||||||
masterKey = handshakeCrypto.deriveMasterKey_0_0(
|
|
||||||
theirStaticPublicKey, theirEphemeralPublicKey,
|
|
||||||
ourStaticKeyPair, ourEphemeralKeyPair, alice);
|
|
||||||
}
|
|
||||||
} catch (GeneralSecurityException e) {
|
} catch (GeneralSecurityException e) {
|
||||||
throw new FormatException();
|
throw new FormatException();
|
||||||
}
|
}
|
||||||
@@ -187,10 +179,11 @@ class HandshakeManagerImpl implements HandshakeManager {
|
|||||||
} else {
|
} else {
|
||||||
// The remote peer did not send a minor version record, so the
|
// The remote peer did not send a minor version record, so the
|
||||||
// remote peer's protocol minor version is assumed to be zero
|
// remote peer's protocol minor version is assumed to be zero
|
||||||
// TODO: Remove this branch after a reasonable migration period
|
|
||||||
// (added 2023-03-10).
|
// TODO: How communicate to user that contact seems to use a version
|
||||||
theirMinorVersion = 0;
|
// of Briar that is too old? (be aware of MITM attacks)
|
||||||
theirEphemeralPublicKey = parsePublicKey(first);
|
// `RendezvousPollerImpl` broadcasts PendingContactState FAILED via EventBus
|
||||||
|
throw new UnsupportedVersionException(true);
|
||||||
}
|
}
|
||||||
return new Pair<>(theirMinorVersion, theirEphemeralPublicKey);
|
return new Pair<>(theirMinorVersion, theirEphemeralPublicKey);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,36 +222,6 @@ class CryptoComponentImpl implements CryptoComponent {
|
|||||||
return new SecretKey(hash);
|
return new SecretKey(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@Deprecated
|
|
||||||
public SecretKey deriveSharedSecretBadly(String label,
|
|
||||||
PublicKey theirStaticPublicKey, PublicKey theirEphemeralPublicKey,
|
|
||||||
KeyPair ourStaticKeyPair, KeyPair ourEphemeralKeyPair,
|
|
||||||
boolean alice, byte[]... inputs) throws GeneralSecurityException {
|
|
||||||
PrivateKey ourStaticPrivateKey = ourStaticKeyPair.getPrivate();
|
|
||||||
PrivateKey ourEphemeralPrivateKey = ourEphemeralKeyPair.getPrivate();
|
|
||||||
byte[][] hashInputs = new byte[inputs.length + 3][];
|
|
||||||
// Alice static/Bob static
|
|
||||||
hashInputs[0] = performRawKeyAgreement(ourStaticPrivateKey,
|
|
||||||
theirStaticPublicKey);
|
|
||||||
// Alice static/Bob ephemeral, Bob static/Alice ephemeral
|
|
||||||
if (alice) {
|
|
||||||
hashInputs[1] = performRawKeyAgreement(ourStaticPrivateKey,
|
|
||||||
theirEphemeralPublicKey);
|
|
||||||
hashInputs[2] = performRawKeyAgreement(ourEphemeralPrivateKey,
|
|
||||||
theirStaticPublicKey);
|
|
||||||
} else {
|
|
||||||
hashInputs[1] = performRawKeyAgreement(ourEphemeralPrivateKey,
|
|
||||||
theirStaticPublicKey);
|
|
||||||
hashInputs[2] = performRawKeyAgreement(ourStaticPrivateKey,
|
|
||||||
theirEphemeralPublicKey);
|
|
||||||
}
|
|
||||||
arraycopy(inputs, 0, hashInputs, 3, inputs.length);
|
|
||||||
byte[] hash = hash(label, hashInputs);
|
|
||||||
if (hash.length != SecretKey.LENGTH) throw new IllegalStateException();
|
|
||||||
return new SecretKey(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SecretKey deriveSharedSecret(String label,
|
public SecretKey deriveSharedSecret(String label,
|
||||||
PublicKey theirStaticPublicKey, PublicKey theirEphemeralPublicKey,
|
PublicKey theirStaticPublicKey, PublicKey theirEphemeralPublicKey,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.briarproject.bramble.contact;
|
package org.briarproject.bramble.contact;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.FormatException;
|
import org.briarproject.bramble.api.FormatException;
|
||||||
|
import org.briarproject.bramble.api.UnsupportedVersionException;
|
||||||
import org.briarproject.bramble.api.contact.ContactManager;
|
import org.briarproject.bramble.api.contact.ContactManager;
|
||||||
import org.briarproject.bramble.api.contact.HandshakeManager.HandshakeResult;
|
import org.briarproject.bramble.api.contact.HandshakeManager.HandshakeResult;
|
||||||
import org.briarproject.bramble.api.contact.PendingContact;
|
import org.briarproject.bramble.api.contact.PendingContact;
|
||||||
@@ -27,6 +28,7 @@ import org.junit.Test;
|
|||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.EOFException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -123,12 +125,12 @@ public class HandshakeManagerImplTest extends BrambleMockTestCase {
|
|||||||
assertEquals(alice, result.isAlice());
|
assertEquals(alice, result.isAlice());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test(expected = UnsupportedVersionException.class)
|
||||||
public void testHandshakeAsAliceWithPeerVersion_0_0() throws Exception {
|
public void testHandshakeAsAliceWithPeerVersion_0_0() throws Exception {
|
||||||
testHandshakeWithPeerVersion_0_0(true);
|
testHandshakeWithPeerVersion_0_0(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test(expected = UnsupportedVersionException.class)
|
||||||
public void testHandshakeAsBobWithPeerVersion_0_0() throws Exception {
|
public void testHandshakeAsBobWithPeerVersion_0_0() throws Exception {
|
||||||
testHandshakeWithPeerVersion_0_0(false);
|
testHandshakeWithPeerVersion_0_0(false);
|
||||||
}
|
}
|
||||||
@@ -140,20 +142,8 @@ public class HandshakeManagerImplTest extends BrambleMockTestCase {
|
|||||||
expectSendKey();
|
expectSendKey();
|
||||||
// Remote peer does not send minor version, so use old key derivation
|
// Remote peer does not send minor version, so use old key derivation
|
||||||
expectReceiveKey();
|
expectReceiveKey();
|
||||||
expectDeriveMasterKey_0_0(alice);
|
|
||||||
expectDeriveProof(alice);
|
|
||||||
expectSendProof();
|
|
||||||
expectReceiveProof();
|
|
||||||
expectSendEof();
|
|
||||||
expectReceiveEof();
|
|
||||||
expectVerifyOwnership(alice, true);
|
|
||||||
|
|
||||||
HandshakeResult result = handshakeManager.handshake(
|
handshakeManager.handshake(pendingContact.getId(), in, streamWriter);
|
||||||
pendingContact.getId(), in, streamWriter);
|
|
||||||
|
|
||||||
assertArrayEquals(masterKey.getBytes(),
|
|
||||||
result.getMasterKey().getBytes());
|
|
||||||
assertEquals(alice, result.isAlice());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = FormatException.class)
|
@Test(expected = FormatException.class)
|
||||||
@@ -241,15 +231,6 @@ public class HandshakeManagerImplTest extends BrambleMockTestCase {
|
|||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void expectDeriveMasterKey_0_0(boolean alice) throws Exception {
|
|
||||||
context.checking(new Expectations() {{
|
|
||||||
oneOf(handshakeCrypto).deriveMasterKey_0_0(theirStaticPublicKey,
|
|
||||||
theirEphemeralPublicKey, ourStaticKeyPair,
|
|
||||||
ourEphemeralKeyPair, alice);
|
|
||||||
will(returnValue(masterKey));
|
|
||||||
}});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void expectDeriveProof(boolean alice) {
|
private void expectDeriveProof(boolean alice) {
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
oneOf(handshakeCrypto).proveOwnership(masterKey, alice);
|
oneOf(handshakeCrypto).proveOwnership(masterKey, alice);
|
||||||
|
|||||||
@@ -60,22 +60,6 @@ public class KeyAgreementTest extends BrambleTestCase {
|
|||||||
assertArrayEquals(aShared.getBytes(), bShared.getBytes());
|
assertArrayEquals(aShared.getBytes(), bShared.getBytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testDerivesStaticEphemeralSharedSecretBadly() throws Exception {
|
|
||||||
String label = getRandomString(123);
|
|
||||||
KeyPair aStatic = crypto.generateAgreementKeyPair();
|
|
||||||
KeyPair aEphemeral = crypto.generateAgreementKeyPair();
|
|
||||||
KeyPair bStatic = crypto.generateAgreementKeyPair();
|
|
||||||
KeyPair bEphemeral = crypto.generateAgreementKeyPair();
|
|
||||||
SecretKey aShared = crypto.deriveSharedSecretBadly(label,
|
|
||||||
bStatic.getPublic(), bEphemeral.getPublic(), aStatic,
|
|
||||||
aEphemeral, true, inputs);
|
|
||||||
SecretKey bShared = crypto.deriveSharedSecretBadly(label,
|
|
||||||
aStatic.getPublic(), aEphemeral.getPublic(), bStatic,
|
|
||||||
bEphemeral, false, inputs);
|
|
||||||
assertArrayEquals(aShared.getBytes(), bShared.getBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDerivesStaticEphemeralSharedSecret() throws Exception {
|
public void testDerivesStaticEphemeralSharedSecret() throws Exception {
|
||||||
String label = getRandomString(123);
|
String label = getRandomString(123);
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ public class ForumActivity extends
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ThreadItemAdapter<ForumPostItem> createAdapter() {
|
protected ThreadItemAdapter<ForumPostItem> createAdapter() {
|
||||||
return new ThreadItemAdapter<>(this, this);
|
return new ThreadItemAdapter<>(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import org.briarproject.briar.api.forum.ForumPostHeader;
|
|||||||
import javax.annotation.concurrent.NotThreadSafe;
|
import javax.annotation.concurrent.NotThreadSafe;
|
||||||
|
|
||||||
@NotThreadSafe
|
@NotThreadSafe
|
||||||
public class ForumPostItem extends ThreadItem {
|
class ForumPostItem extends ThreadItem {
|
||||||
|
|
||||||
ForumPostItem(ForumPostHeader h) {
|
ForumPostItem(ForumPostHeader h, String text) {
|
||||||
super(h.getId(), h.getParentId(), h.getTimestamp(), h.getAuthor(),
|
super(h.getId(), h.getParentId(), text, h.getTimestamp(), h.getAuthor(),
|
||||||
h.getAuthorInfo(), h.isRead());
|
h.getAuthorInfo(), h.isRead());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,9 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
|
|||||||
ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
|
ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
|
||||||
if (f.getGroupId().equals(groupId)) {
|
if (f.getGroupId().equals(groupId)) {
|
||||||
LOG.info("Forum post received, adding...");
|
LOG.info("Forum post received, adding...");
|
||||||
addItem(new ForumPostItem(f.getHeader()), false);
|
ForumPostItem item =
|
||||||
|
new ForumPostItem(f.getHeader(), f.getText());
|
||||||
|
addItem(item, false);
|
||||||
}
|
}
|
||||||
} else if (e instanceof ForumInvitationResponseReceivedEvent) {
|
} else if (e instanceof ForumInvitationResponseReceivedEvent) {
|
||||||
ForumInvitationResponseReceivedEvent f =
|
ForumInvitationResponseReceivedEvent f =
|
||||||
@@ -137,14 +139,22 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
|
|||||||
List<ForumPostHeader> headers =
|
List<ForumPostHeader> headers =
|
||||||
forumManager.getPostHeaders(txn, groupId);
|
forumManager.getPostHeaders(txn, groupId);
|
||||||
logDuration(LOG, "Loading headers", start);
|
logDuration(LOG, "Loading headers", start);
|
||||||
|
start = now();
|
||||||
List<ForumPostItem> items = new ArrayList<>();
|
List<ForumPostItem> items = new ArrayList<>();
|
||||||
for (ForumPostHeader header : headers) {
|
for (ForumPostHeader header : headers) {
|
||||||
items.add(new ForumPostItem(header));
|
items.add(loadItem(txn, header));
|
||||||
}
|
}
|
||||||
|
logDuration(LOG, "Loading bodies and creating items", start);
|
||||||
return items;
|
return items;
|
||||||
}, this::setItems);
|
}, this::setItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ForumPostItem loadItem(Transaction txn, ForumPostHeader header)
|
||||||
|
throws DbException {
|
||||||
|
String text = forumManager.getPostText(txn, header.getId());
|
||||||
|
return new ForumPostItem(header, text);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void createAndStoreMessage(String text,
|
public void createAndStoreMessage(String text,
|
||||||
@Nullable MessageId parentId) {
|
@Nullable MessageId parentId) {
|
||||||
@@ -165,17 +175,21 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
|
|||||||
@Nullable MessageId parentId, LocalAuthor author) {
|
@Nullable MessageId parentId, LocalAuthor author) {
|
||||||
cryptoExecutor.execute(() -> {
|
cryptoExecutor.execute(() -> {
|
||||||
LOG.info("Creating forum post...");
|
LOG.info("Creating forum post...");
|
||||||
storePost(forumManager.createLocalPost(groupId, text,
|
ForumPost msg = forumManager.createLocalPost(groupId, text,
|
||||||
timestamp, parentId, author));
|
timestamp, parentId, author);
|
||||||
|
storePost(msg, text);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void storePost(ForumPost msg) {
|
private void storePost(ForumPost msg, String text) {
|
||||||
runOnDbThread(false, txn -> {
|
runOnDbThread(false, txn -> {
|
||||||
long start = now();
|
long start = now();
|
||||||
ForumPostHeader header = forumManager.addLocalPost(txn, msg);
|
ForumPostHeader header = forumManager.addLocalPost(txn, msg);
|
||||||
logDuration(LOG, "Storing forum post", start);
|
logDuration(LOG, "Storing forum post", start);
|
||||||
txn.attach(() -> addItem(new ForumPostItem(header), true));
|
txn.attach(() -> {
|
||||||
|
ForumPostItem item = new ForumPostItem(header, text);
|
||||||
|
addItem(item, true);
|
||||||
|
});
|
||||||
}, this::handleException);
|
}, this::handleException);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,9 +229,4 @@ class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getMessageText(Transaction txn, MessageId m)
|
|
||||||
throws DbException {
|
|
||||||
return forumManager.getPostText(txn, m);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.briarproject.briar.android.privategroup.memberlist.GroupMemberListAct
|
|||||||
import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
|
import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
|
||||||
import org.briarproject.briar.android.threaded.ThreadListActivity;
|
import org.briarproject.briar.android.threaded.ThreadListActivity;
|
||||||
import org.briarproject.briar.android.threaded.ThreadListViewModel;
|
import org.briarproject.briar.android.threaded.ThreadListViewModel;
|
||||||
|
import org.briarproject.briar.android.widget.LinkDialogFragment;
|
||||||
import org.briarproject.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.nullsafety.ParametersNotNullByDefault;
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ public class GroupActivity extends
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected GroupMessageAdapter createAdapter() {
|
protected GroupMessageAdapter createAdapter() {
|
||||||
return new GroupMessageAdapter(this, this);
|
return new GroupMessageAdapter(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -159,6 +160,12 @@ public class GroupActivity extends
|
|||||||
if (isDissolved != null && !isDissolved) super.onReplyClick(item);
|
if (isDissolved != null && !isDissolved) super.onReplyClick(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLinkClick(String url){
|
||||||
|
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
|
||||||
|
f.show(getSupportFragmentManager(), f.getUniqueTag());
|
||||||
|
}
|
||||||
|
|
||||||
private void setGroupEnabled(boolean enabled) {
|
private void setGroupEnabled(boolean enabled) {
|
||||||
sendController.setReady(enabled);
|
sendController.setReady(enabled);
|
||||||
list.getRecyclerView().setAlpha(enabled ? 1f : 0.5f);
|
list.getRecyclerView().setAlpha(enabled ? 1f : 0.5f);
|
||||||
|
|||||||
@@ -11,19 +11,16 @@ import org.briarproject.briar.android.threaded.ThreadPostViewHolder;
|
|||||||
import org.briarproject.nullsafety.NotNullByDefault;
|
import org.briarproject.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
import androidx.annotation.LayoutRes;
|
import androidx.annotation.LayoutRes;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
|
class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
|
||||||
|
|
||||||
private boolean isCreator = false;
|
private boolean isCreator = false;
|
||||||
|
|
||||||
GroupMessageAdapter(LifecycleOwner lifecycleOwner,
|
GroupMessageAdapter(ThreadItemListener<GroupMessageItem> listener) {
|
||||||
ThreadItemListener<GroupMessageItem> listener) {
|
super(listener);
|
||||||
super(lifecycleOwner, listener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@LayoutRes
|
@LayoutRes
|
||||||
@@ -33,7 +30,6 @@ public class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
|
|||||||
return item.getLayout();
|
return item.getLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
@Override
|
||||||
public BaseThreadItemViewHolder<GroupMessageItem> onCreateViewHolder(
|
public BaseThreadItemViewHolder<GroupMessageItem> onCreateViewHolder(
|
||||||
ViewGroup parent, int type) {
|
ViewGroup parent, int type) {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
package org.briarproject.briar.android.privategroup.conversation;
|
package org.briarproject.briar.android.privategroup.conversation;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.identity.Author;
|
||||||
|
import org.briarproject.briar.api.identity.AuthorInfo;
|
||||||
import org.briarproject.bramble.api.sync.GroupId;
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.threaded.ThreadItem;
|
import org.briarproject.briar.android.threaded.ThreadItem;
|
||||||
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
|
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import javax.annotation.concurrent.NotThreadSafe;
|
import javax.annotation.concurrent.NotThreadSafe;
|
||||||
|
|
||||||
import androidx.annotation.LayoutRes;
|
import androidx.annotation.LayoutRes;
|
||||||
@@ -12,14 +16,20 @@ import androidx.annotation.UiThread;
|
|||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
@NotThreadSafe
|
@NotThreadSafe
|
||||||
public class GroupMessageItem extends ThreadItem {
|
class GroupMessageItem extends ThreadItem {
|
||||||
|
|
||||||
private final GroupId groupId;
|
private final GroupId groupId;
|
||||||
|
|
||||||
GroupMessageItem(GroupMessageHeader h) {
|
private GroupMessageItem(MessageId messageId, GroupId groupId,
|
||||||
super(h.getId(), h.getParentId(), h.getTimestamp(), h.getAuthor(),
|
@Nullable MessageId parentId, String text, long timestamp,
|
||||||
h.getAuthorInfo(), h.isRead());
|
Author author, AuthorInfo authorInfo, boolean isRead) {
|
||||||
this.groupId = h.getGroupId();
|
super(messageId, parentId, text, timestamp, author, authorInfo, isRead);
|
||||||
|
this.groupId = groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupMessageItem(GroupMessageHeader h, String text) {
|
||||||
|
this(h.getId(), h.getGroupId(), h.getParentId(), text, h.getTimestamp(),
|
||||||
|
h.getAuthor(), h.getAuthorInfo(), h.isRead());
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupId getGroupId() {
|
public GroupId getGroupId() {
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
|
|||||||
// only act on non-local messages in this group
|
// only act on non-local messages in this group
|
||||||
if (!g.isLocal() && g.getGroupId().equals(groupId)) {
|
if (!g.isLocal() && g.getGroupId().equals(groupId)) {
|
||||||
LOG.info("Group message received, adding...");
|
LOG.info("Group message received, adding...");
|
||||||
GroupMessageItem item = buildItem(g.getHeader());
|
GroupMessageItem item = buildItem(g.getHeader(), g.getText());
|
||||||
addItem(item, false);
|
addItem(item, false);
|
||||||
// In case the join message comes from the creator,
|
// In case the join message comes from the creator,
|
||||||
// we need to reload the sharing contacts
|
// we need to reload the sharing contacts
|
||||||
@@ -167,19 +167,33 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
|
|||||||
List<GroupMessageHeader> headers =
|
List<GroupMessageHeader> headers =
|
||||||
privateGroupManager.getHeaders(txn, groupId);
|
privateGroupManager.getHeaders(txn, groupId);
|
||||||
logDuration(LOG, "Loading headers", start);
|
logDuration(LOG, "Loading headers", start);
|
||||||
|
start = now();
|
||||||
List<GroupMessageItem> items = new ArrayList<>();
|
List<GroupMessageItem> items = new ArrayList<>();
|
||||||
for (GroupMessageHeader header : headers) {
|
for (GroupMessageHeader header : headers) {
|
||||||
items.add(buildItem(header));
|
items.add(loadItem(txn, header));
|
||||||
}
|
}
|
||||||
|
logDuration(LOG, "Loading bodies and creating items", start);
|
||||||
return items;
|
return items;
|
||||||
}, this::setItems);
|
}, this::setItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
private GroupMessageItem buildItem(GroupMessageHeader header) {
|
private GroupMessageItem loadItem(Transaction txn,
|
||||||
|
GroupMessageHeader header) throws DbException {
|
||||||
|
String text;
|
||||||
if (header instanceof JoinMessageHeader) {
|
if (header instanceof JoinMessageHeader) {
|
||||||
return new JoinMessageItem((JoinMessageHeader) header);
|
// will be looked up later
|
||||||
|
text = "";
|
||||||
|
} else {
|
||||||
|
text = privateGroupManager.getMessageText(txn, header.getId());
|
||||||
}
|
}
|
||||||
return new GroupMessageItem(header);
|
return buildItem(header, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupMessageItem buildItem(GroupMessageHeader header, String text) {
|
||||||
|
if (header instanceof JoinMessageHeader) {
|
||||||
|
return new JoinMessageItem((JoinMessageHeader) header, text);
|
||||||
|
}
|
||||||
|
return new GroupMessageItem(header, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -207,17 +221,19 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
|
|||||||
LOG.info("Creating group message...");
|
LOG.info("Creating group message...");
|
||||||
GroupMessage msg = groupMessageFactory.createGroupMessage(groupId,
|
GroupMessage msg = groupMessageFactory.createGroupMessage(groupId,
|
||||||
timestamp, parentId, author, text, previousMsgId);
|
timestamp, parentId, author, text, previousMsgId);
|
||||||
storePost(msg);
|
storePost(msg, text);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void storePost(GroupMessage msg) {
|
private void storePost(GroupMessage msg, String text) {
|
||||||
runOnDbThread(false, txn -> {
|
runOnDbThread(false, txn -> {
|
||||||
long start = now();
|
long start = now();
|
||||||
GroupMessageHeader header =
|
GroupMessageHeader header =
|
||||||
privateGroupManager.addLocalMessage(txn, msg);
|
privateGroupManager.addLocalMessage(txn, msg);
|
||||||
logDuration(LOG, "Storing group message", start);
|
logDuration(LOG, "Storing group message", start);
|
||||||
txn.attach(() -> addItem(buildItem(header), true));
|
txn.attach(() ->
|
||||||
|
addItem(buildItem(header, text), true)
|
||||||
|
);
|
||||||
}, this::handleException);
|
}, this::handleException);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,9 +284,4 @@ class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
|
|||||||
return isDissolved;
|
return isDissolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected String getMessageText(Transaction txn, MessageId m)
|
|
||||||
throws DbException {
|
|
||||||
return privateGroupManager.getMessageText(txn, m);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ class JoinMessageItem extends GroupMessageItem {
|
|||||||
|
|
||||||
private final boolean isInitial;
|
private final boolean isInitial;
|
||||||
|
|
||||||
JoinMessageItem(JoinMessageHeader h) {
|
JoinMessageItem(JoinMessageHeader h, String text) {
|
||||||
super(h);
|
super(h, text);
|
||||||
isInitial = h.isInitial();
|
isInitial = h.isInitial();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListe
|
|||||||
import org.briarproject.nullsafety.NotNullByDefault;
|
import org.briarproject.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
|
||||||
|
|
||||||
import static org.briarproject.briar.api.identity.AuthorInfo.Status.OURSELVES;
|
import static org.briarproject.briar.api.identity.AuthorInfo.Status.OURSELVES;
|
||||||
|
|
||||||
@@ -26,8 +25,10 @@ class JoinMessageItemViewHolder
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setText(GroupMessageItem item, LifecycleOwner lifecycleOwner,
|
public void bind(GroupMessageItem item,
|
||||||
ThreadItemListener<GroupMessageItem> listener) {
|
ThreadItemListener<GroupMessageItem> listener) {
|
||||||
|
super.bind(item, listener);
|
||||||
|
|
||||||
if (isCreator) bindForCreator((JoinMessageItem) item);
|
if (isCreator) bindForCreator((JoinMessageItem) item);
|
||||||
else bind((JoinMessageItem) item);
|
else bind((JoinMessageItem) item);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,46 +4,35 @@ import android.animation.Animator;
|
|||||||
import android.animation.ArgbEvaluator;
|
import android.animation.ArgbEvaluator;
|
||||||
import android.animation.ValueAnimator;
|
import android.animation.ValueAnimator;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.text.util.Linkify;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.animation.AccelerateInterpolator;
|
import android.view.animation.AccelerateInterpolator;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.util.StringUtils;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener;
|
import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener;
|
||||||
import org.briarproject.briar.android.view.AuthorView;
|
import org.briarproject.briar.android.view.AuthorView;
|
||||||
import org.briarproject.nullsafety.NotNullByDefault;
|
import org.briarproject.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
import androidx.annotation.CallSuper;
|
import androidx.annotation.CallSuper;
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.lifecycle.Observer;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import static android.text.util.Linkify.WEB_URLS;
|
|
||||||
import static android.text.util.Linkify.addLinks;
|
|
||||||
import static androidx.core.content.ContextCompat.getColor;
|
import static androidx.core.content.ContextCompat.getColor;
|
||||||
import static org.briarproject.bramble.util.StringUtils.trim;
|
|
||||||
import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable;
|
import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable;
|
||||||
import static org.briarproject.nullsafety.NullSafety.requireNonNull;
|
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
|
public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
|
||||||
extends RecyclerView.ViewHolder implements Observer<String> {
|
extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
private final static int ANIMATION_DURATION = 5000;
|
private final static int ANIMATION_DURATION = 5000;
|
||||||
|
|
||||||
protected final TextView textView;
|
protected final TextView textView;
|
||||||
private final ViewGroup layout;
|
private final ViewGroup layout;
|
||||||
private final AuthorView author;
|
private final AuthorView author;
|
||||||
@Nullable
|
|
||||||
private ThreadItemListener<I> listener = null;
|
|
||||||
@Nullable
|
|
||||||
private LiveData<String> textLiveData = null;
|
|
||||||
|
|
||||||
public BaseThreadItemViewHolder(View v) {
|
public BaseThreadItemViewHolder(View v) {
|
||||||
super(v);
|
super(v);
|
||||||
@@ -54,9 +43,10 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
public void bind(I item, LifecycleOwner lifecycleOwner,
|
public void bind(I item, ThreadItemListener<I> listener) {
|
||||||
ThreadItemListener<I> listener) {
|
textView.setText(StringUtils.trim(item.getText()));
|
||||||
setText(item, lifecycleOwner, listener);
|
Linkify.addLinks(textView, Linkify.WEB_URLS);
|
||||||
|
makeLinksClickable(textView, listener::onLinkClick);
|
||||||
|
|
||||||
author.setAuthor(item.getAuthor(), item.getAuthorInfo());
|
author.setAuthor(item.getAuthor(), item.getAuthorInfo());
|
||||||
author.setDate(item.getTimestamp());
|
author.setDate(item.getTimestamp());
|
||||||
@@ -71,20 +61,6 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setText(I item, LifecycleOwner lifecycleOwner,
|
|
||||||
ThreadItemListener<I> listener) {
|
|
||||||
// Clear any existing text while we asynchronously load the new text
|
|
||||||
textView.setText(null);
|
|
||||||
// Remember the listener so we can use it to create links later
|
|
||||||
this.listener = listener;
|
|
||||||
// If the view has been re-bound and we're already asynchronously
|
|
||||||
// loading text for another item, stop observing it
|
|
||||||
if (textLiveData != null) textLiveData.removeObserver(this);
|
|
||||||
// Asynchronously load the text for this item and observe the result
|
|
||||||
textLiveData = listener.loadItemText(item.getId());
|
|
||||||
textLiveData.observe(lifecycleOwner, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void animateFadeOut() {
|
private void animateFadeOut() {
|
||||||
setIsRecyclable(false);
|
setIsRecyclable(false);
|
||||||
ValueAnimator anim = new ValueAnimator();
|
ValueAnimator anim = new ValueAnimator();
|
||||||
@@ -97,7 +73,6 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
|
|||||||
@Override
|
@Override
|
||||||
public void onAnimationStart(Animator animation) {
|
public void onAnimationStart(Animator animation) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAnimationEnd(Animator animation) {
|
public void onAnimationEnd(Animator animation) {
|
||||||
layout.setBackgroundResource(
|
layout.setBackgroundResource(
|
||||||
@@ -105,11 +80,9 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
|
|||||||
layout.setActivated(false);
|
layout.setActivated(false);
|
||||||
setIsRecyclable(true);
|
setIsRecyclable(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAnimationCancel(Animator animation) {
|
public void onAnimationCancel(Animator animation) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAnimationRepeat(Animator animation) {
|
public void onAnimationRepeat(Animator animation) {
|
||||||
}
|
}
|
||||||
@@ -124,24 +97,4 @@ public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
|
|||||||
return textView.getContext();
|
return textView.getContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onViewRecycled() {
|
|
||||||
textView.setText(null);
|
|
||||||
if (textLiveData != null) {
|
|
||||||
textLiveData.removeObserver(this);
|
|
||||||
textLiveData = null;
|
|
||||||
listener = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onChanged(String s) {
|
|
||||||
if (textLiveData != null) {
|
|
||||||
textLiveData.removeObserver(this);
|
|
||||||
textLiveData = null;
|
|
||||||
textView.setText(trim(s));
|
|
||||||
addLinks(textView, WEB_URLS);
|
|
||||||
makeLinksClickable(textView, requireNonNull(listener)::onLinkClick);
|
|
||||||
listener = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public abstract class ThreadItem implements MessageNode {
|
|||||||
private final MessageId messageId;
|
private final MessageId messageId;
|
||||||
@Nullable
|
@Nullable
|
||||||
private final MessageId parentId;
|
private final MessageId parentId;
|
||||||
|
private final String text;
|
||||||
private final long timestamp;
|
private final long timestamp;
|
||||||
private final Author author;
|
private final Author author;
|
||||||
private final AuthorInfo authorInfo;
|
private final AuthorInfo authorInfo;
|
||||||
@@ -26,10 +27,11 @@ public abstract class ThreadItem implements MessageNode {
|
|||||||
private boolean isRead, highlighted;
|
private boolean isRead, highlighted;
|
||||||
|
|
||||||
public ThreadItem(MessageId messageId, @Nullable MessageId parentId,
|
public ThreadItem(MessageId messageId, @Nullable MessageId parentId,
|
||||||
long timestamp, Author author, AuthorInfo authorInfo,
|
String text, long timestamp, Author author, AuthorInfo authorInfo,
|
||||||
boolean isRead) {
|
boolean isRead) {
|
||||||
this.messageId = messageId;
|
this.messageId = messageId;
|
||||||
this.parentId = parentId;
|
this.parentId = parentId;
|
||||||
|
this.text = text;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.author = author;
|
this.author = author;
|
||||||
this.authorInfo = authorInfo;
|
this.authorInfo = authorInfo;
|
||||||
@@ -37,6 +39,10 @@ public abstract class ThreadItem implements MessageNode {
|
|||||||
this.highlighted = false;
|
this.highlighted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getText() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
public int getLevel() {
|
public int getLevel() {
|
||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import javax.annotation.Nullable;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.recyclerview.widget.DiffUtil;
|
import androidx.recyclerview.widget.DiffUtil;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.ListAdapter;
|
import androidx.recyclerview.widget.ListAdapter;
|
||||||
@@ -29,11 +27,9 @@ public class ThreadItemAdapter<I extends ThreadItem>
|
|||||||
|
|
||||||
static final int UNDEFINED = -1;
|
static final int UNDEFINED = -1;
|
||||||
|
|
||||||
private final LifecycleOwner lifecycleOwner;
|
|
||||||
private final ThreadItemListener<I> listener;
|
private final ThreadItemListener<I> listener;
|
||||||
|
|
||||||
public ThreadItemAdapter(LifecycleOwner lifecycleOwner,
|
public ThreadItemAdapter(ThreadItemListener<I> listener) {
|
||||||
ThreadItemListener<I> listener) {
|
|
||||||
super(new DiffUtil.ItemCallback<I>() {
|
super(new DiffUtil.ItemCallback<I>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean areItemsTheSame(I a, I b) {
|
public boolean areItemsTheSame(I a, I b) {
|
||||||
@@ -46,7 +42,6 @@ public class ThreadItemAdapter<I extends ThreadItem>
|
|||||||
a.isRead() == b.isRead();
|
a.isRead() == b.isRead();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.lifecycleOwner = lifecycleOwner;
|
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +58,7 @@ public class ThreadItemAdapter<I extends ThreadItem>
|
|||||||
public void onBindViewHolder(@NonNull BaseThreadItemViewHolder<I> ui,
|
public void onBindViewHolder(@NonNull BaseThreadItemViewHolder<I> ui,
|
||||||
int position) {
|
int position) {
|
||||||
I item = getItem(position);
|
I item = getItem(position);
|
||||||
ui.bind(item, lifecycleOwner, listener);
|
ui.bind(item, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
int findItemPosition(MessageId id) {
|
int findItemPosition(MessageId id) {
|
||||||
@@ -140,19 +135,9 @@ public class ThreadItemAdapter<I extends ThreadItem>
|
|||||||
return getItem(position);
|
return getItem(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewRecycled(BaseThreadItemViewHolder<I> viewHolder) {
|
|
||||||
super.onViewRecycled(viewHolder);
|
|
||||||
viewHolder.onViewRecycled();
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface ThreadItemListener<I> {
|
public interface ThreadItemListener<I> {
|
||||||
|
|
||||||
void onReplyClick(I item);
|
void onReplyClick(I item);
|
||||||
|
|
||||||
void onLinkClick(String url);
|
void onLinkClick(String url);
|
||||||
|
|
||||||
LiveData<String> loadItemText(MessageId m);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,9 +85,6 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
|
|||||||
scrollListener = new ThreadScrollListener<>(adapter, viewModel,
|
scrollListener = new ThreadScrollListener<>(adapter, viewModel,
|
||||||
upButton, downButton);
|
upButton, downButton);
|
||||||
list.getRecyclerView().addOnScrollListener(scrollListener);
|
list.getRecyclerView().addOnScrollListener(scrollListener);
|
||||||
// This is a tradeoff between memory consumption for cached views
|
|
||||||
// and the cost of loading message text from the database
|
|
||||||
list.getRecyclerView().setItemViewCacheSize(20);
|
|
||||||
|
|
||||||
upButton.setOnClickListener(v -> {
|
upButton.setOnClickListener(v -> {
|
||||||
int position = adapter.getVisibleUnreadPosTop(layoutManager);
|
int position = adapter.getVisibleUnreadPosTop(layoutManager);
|
||||||
@@ -260,8 +257,4 @@ public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadI
|
|||||||
|
|
||||||
protected abstract int getMaxTextLength();
|
protected abstract int getMaxTextLength();
|
||||||
|
|
||||||
@Override
|
|
||||||
public LiveData<String> loadItemText(MessageId m) {
|
|
||||||
return getViewModel().loadMessageText(m);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import org.briarproject.bramble.api.crypto.CryptoExecutor;
|
|||||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
import org.briarproject.bramble.api.db.NoSuchGroupException;
|
import org.briarproject.bramble.api.db.NoSuchGroupException;
|
||||||
import org.briarproject.bramble.api.db.Transaction;
|
|
||||||
import org.briarproject.bramble.api.db.TransactionManager;
|
import org.briarproject.bramble.api.db.TransactionManager;
|
||||||
import org.briarproject.bramble.api.event.Event;
|
import org.briarproject.bramble.api.event.Event;
|
||||||
import org.briarproject.bramble.api.event.EventBus;
|
import org.briarproject.bramble.api.event.EventBus;
|
||||||
@@ -261,14 +260,4 @@ public abstract class ThreadListViewModel<I extends ThreadItem>
|
|||||||
return scrollToItem.getAndSet(null);
|
return scrollToItem.getAndSet(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<String> loadMessageText(MessageId m) {
|
|
||||||
MutableLiveData<String> textLiveData = new MutableLiveData<>();
|
|
||||||
runOnDbThread(true, txn ->
|
|
||||||
textLiveData.postValue(getMessageText(txn, m)),
|
|
||||||
this::handleException);
|
|
||||||
return textLiveData;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract String getMessageText(Transaction txn, MessageId m)
|
|
||||||
throws DbException;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import org.briarproject.nullsafety.NotNullByDefault;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
|
||||||
|
|
||||||
import static android.view.View.GONE;
|
import static android.view.View.GONE;
|
||||||
import static android.view.View.VISIBLE;
|
import static android.view.View.VISIBLE;
|
||||||
@@ -41,9 +40,8 @@ public class ThreadPostViewHolder<I extends ThreadItem>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void bind(I item, LifecycleOwner lifecycleOwner,
|
public void bind(I item, ThreadItemListener<I> listener) {
|
||||||
ThreadItemListener<I> listener) {
|
super.bind(item, listener);
|
||||||
super.bind(item, lifecycleOwner, listener);
|
|
||||||
|
|
||||||
for (int i = 0; i < lvls.length; i++) {
|
for (int i = 0; i < lvls.length; i++) {
|
||||||
lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE);
|
lvls[i].setVisibility(i < item.getLevel() ? VISIBLE : GONE);
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ public class ForumPostReceivedEvent extends Event {
|
|||||||
|
|
||||||
private final GroupId groupId;
|
private final GroupId groupId;
|
||||||
private final ForumPostHeader header;
|
private final ForumPostHeader header;
|
||||||
|
private final String text;
|
||||||
|
|
||||||
public ForumPostReceivedEvent(GroupId groupId, ForumPostHeader header) {
|
public ForumPostReceivedEvent(GroupId groupId, ForumPostHeader header,
|
||||||
|
String text) {
|
||||||
this.groupId = groupId;
|
this.groupId = groupId;
|
||||||
this.header = header;
|
this.header = header;
|
||||||
|
this.text = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupId getGroupId() {
|
public GroupId getGroupId() {
|
||||||
@@ -29,4 +32,8 @@ public class ForumPostReceivedEvent extends Event {
|
|||||||
public ForumPostHeader getHeader() {
|
public ForumPostHeader getHeader() {
|
||||||
return header;
|
return header;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getText() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ public class GroupMessageAddedEvent extends Event {
|
|||||||
|
|
||||||
private final GroupId groupId;
|
private final GroupId groupId;
|
||||||
private final GroupMessageHeader header;
|
private final GroupMessageHeader header;
|
||||||
|
private final String text;
|
||||||
private final boolean local;
|
private final boolean local;
|
||||||
|
|
||||||
public GroupMessageAddedEvent(GroupId groupId, GroupMessageHeader header,
|
public GroupMessageAddedEvent(GroupId groupId, GroupMessageHeader header,
|
||||||
boolean local) {
|
String text, boolean local) {
|
||||||
this.groupId = groupId;
|
this.groupId = groupId;
|
||||||
this.header = header;
|
this.header = header;
|
||||||
|
this.text = text;
|
||||||
this.local = local;
|
this.local = local;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +36,10 @@ public class GroupMessageAddedEvent extends Event {
|
|||||||
return header;
|
return header;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getText() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isLocal() {
|
public boolean isLocal() {
|
||||||
return local;
|
return local;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,10 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
|
|||||||
messageTracker.trackIncomingMessage(txn, m);
|
messageTracker.trackIncomingMessage(txn, m);
|
||||||
|
|
||||||
ForumPostHeader header = getForumPostHeader(txn, m.getId(), meta);
|
ForumPostHeader header = getForumPostHeader(txn, m.getId(), meta);
|
||||||
txn.attach(new ForumPostReceivedEvent(m.getGroupId(), header));
|
String text = getPostText(body);
|
||||||
|
ForumPostReceivedEvent event =
|
||||||
|
new ForumPostReceivedEvent(m.getGroupId(), header, text);
|
||||||
|
txn.attach(event);
|
||||||
|
|
||||||
return ACCEPT_SHARE;
|
return ACCEPT_SHARE;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,6 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
|
|||||||
txn -> getPreviousMsgId(txn, g));
|
txn -> getPreviousMsgId(txn, g));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public MessageId getPreviousMsgId(Transaction txn, GroupId g)
|
public MessageId getPreviousMsgId(Transaction txn, GroupId g)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
try {
|
try {
|
||||||
@@ -606,7 +605,9 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
|
|||||||
throws DbException, FormatException {
|
throws DbException, FormatException {
|
||||||
GroupMessageHeader header = getGroupMessageHeader(txn, m.getGroupId(),
|
GroupMessageHeader header = getGroupMessageHeader(txn, m.getGroupId(),
|
||||||
m.getId(), meta, Collections.emptyMap());
|
m.getId(), meta, Collections.emptyMap());
|
||||||
txn.attach(new GroupMessageAddedEvent(m.getGroupId(), header, local));
|
String text = getMessageText(clientHelper.toList(m));
|
||||||
|
txn.attach(new GroupMessageAddedEvent(m.getGroupId(), header, text,
|
||||||
|
local));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void attachJoinMessageAddedEvent(Transaction txn, Message m,
|
private void attachJoinMessageAddedEvent(Transaction txn, Message m,
|
||||||
@@ -614,7 +615,8 @@ class PrivateGroupManagerImpl extends BdfIncomingMessageHook
|
|||||||
throws DbException, FormatException {
|
throws DbException, FormatException {
|
||||||
JoinMessageHeader header = getJoinMessageHeader(txn, m.getGroupId(),
|
JoinMessageHeader header = getJoinMessageHeader(txn, m.getGroupId(),
|
||||||
m.getId(), meta, Collections.emptyMap());
|
m.getId(), meta, Collections.emptyMap());
|
||||||
txn.attach(new GroupMessageAddedEvent(m.getGroupId(), header, local));
|
txn.attach(new GroupMessageAddedEvent(m.getGroupId(), header, "",
|
||||||
|
local));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addMember(Transaction txn, GroupId g, Author a, Visibility v)
|
private void addMember(Transaction txn, GroupId g, Author a, Visibility v)
|
||||||
|
|||||||
Reference in New Issue
Block a user