diff --git a/README.MD b/README.MD index fca67c1fac..a64b897c84 100644 --- a/README.MD +++ b/README.MD @@ -48,7 +48,7 @@ Our [Actor Bootstrap](https://github.com/actorapp/actor-bootstrap) repository co # Community Support -Keep in touch with the Actor community in our [group chat](https://quit.email/join/0d43e6a90d108ad9608514b5c17b76d5b2721d5e2ea51058d6ca43a66befe7f4). +Keep in touch with the Actor community in our [group chat](https://actor.im/join/actor_oss). # Contacts diff --git a/actor-keygen/Dockerfile b/actor-keygen/Dockerfile new file mode 100644 index 0000000000..9b81a26b14 --- /dev/null +++ b/actor-keygen/Dockerfile @@ -0,0 +1,9 @@ +FROM actor/base-java:latest +MAINTAINER Steve Kite + +ADD build/docker/bin/* /opt/actor-keygen/bin/ +ADD build/docker/lib/* /opt/actor-keygen/lib/ + +WORKDIR "/keygen" + +CMD ["/opt/actor-keygen/bin/actor-keygen"] \ No newline at end of file diff --git a/actor-keygen/build.gradle b/actor-keygen/build.gradle new file mode 100644 index 0000000000..0a0000e3bb --- /dev/null +++ b/actor-keygen/build.gradle @@ -0,0 +1,16 @@ +apply plugin: 'application' + +mainClassName = "im.actor.keygen.Main" + +dependencies { + compile group: 'commons-io', name: 'commons-io', version: '2.5' + testCompile "junit:junit:4.11" +} + +allprojects { + tasks.withType(JavaCompile) { + sourceCompatibility = "1.8" + targetCompatibility = "1.8" + } +} + diff --git a/actor-keygen/docker.sh b/actor-keygen/docker.sh new file mode 100755 index 0000000000..566f23c5ec --- /dev/null +++ b/actor-keygen/docker.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +cd .. +./gradlew actor-keygen:assembleDist +cd actor-keygen + +# Unpacking Distrib +cd build +rm -fr docker +mkdir -p docker +cd distributions +rm -fr actor-keygen +unzip actor-keygen.zip +cp -r actor-keygen/* ../docker/ +cd ../.. + +# Building docker +docker build -t actor/keygen . \ No newline at end of file diff --git a/actor-keygen/src/main/java/im/actor/keygen/Curve25519.java b/actor-keygen/src/main/java/im/actor/keygen/Curve25519.java new file mode 100644 index 0000000000..800380502b --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/Curve25519.java @@ -0,0 +1,64 @@ +package im.actor.keygen; + +import java.security.DigestException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import im.actor.keygen.curve25519.curve_sigs; + +public final class Curve25519 { + + /** + * Generating KeyPair + * + * @param randomBytes 32 random bytes + * @return generated key pair + */ + public static Curve25519KeyPair keyGen(byte[] randomBytes) throws NoSuchAlgorithmException, DigestException { + byte[] privateKey = keyGenPrivate(randomBytes); + byte[] publicKey = keyGenPublic(privateKey); + return new Curve25519KeyPair(publicKey, privateKey); + } + + /** + * Generating private key. Source: https://cr.yp.to/ecdh.html + * + * @param randomBytes random bytes (32+ bytes) + * @return generated private key + */ + public static byte[] keyGenPrivate(byte[] randomBytes) throws NoSuchAlgorithmException, DigestException { + + if (randomBytes.length < 32) { + throw new RuntimeException("Random bytes too small"); + } + + // Hashing Random Bytes instead of using random bytes directly + // Just in case as reference ed255519 implementation do same + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.digest(randomBytes, 0, randomBytes.length); + byte[] privateKey = digest.digest(); + + // Performing bit's flipping + privateKey[0] &= 248; + privateKey[31] &= 127; + privateKey[31] |= 64; + + return privateKey; + } + + /** + * Building public key with private key + * + * @param privateKey private key + * @return generated public key + */ + public static byte[] keyGenPublic(byte[] privateKey) { + byte[] publicKey = new byte[32]; + curve_sigs.curve25519_keygen(publicKey, privateKey); + return publicKey; + } + + private Curve25519() { + + } +} \ No newline at end of file diff --git a/actor-keygen/src/main/java/im/actor/keygen/Curve25519KeyPair.java b/actor-keygen/src/main/java/im/actor/keygen/Curve25519KeyPair.java new file mode 100644 index 0000000000..ac1851c029 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/Curve25519KeyPair.java @@ -0,0 +1,26 @@ +package im.actor.keygen; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class Curve25519KeyPair { + + private byte[] publicKey; + private byte[] privateKey; + + public Curve25519KeyPair(byte[] publicKey, byte[] privateKey) { + this.publicKey = publicKey; + this.privateKey = privateKey; + } + + public byte[] getPublicKey() { + return publicKey; + } + + public byte[] getPrivateKey() { + return privateKey; + } +} \ No newline at end of file diff --git a/actor-keygen/src/main/java/im/actor/keygen/Hex.java b/actor-keygen/src/main/java/im/actor/keygen/Hex.java new file mode 100644 index 0000000000..c40f423069 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/Hex.java @@ -0,0 +1,52 @@ +package im.actor.keygen; + +public class Hex { + + final protected static char[] HEXES_SMALL = "0123456789abcdef".toCharArray(); + + private static final String HEXES = "0123456789ABCDEF"; + + public static String toHex(byte[] raw) { + final StringBuilder hex = new StringBuilder(2 * raw.length); + for (final byte b : raw) { + hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F))); + } + return hex.toString(); + } + + + /** + * Calculating lowcase hex string + * + * @param bytes data for hex + * @return hex string + */ + public static String hex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEXES_SMALL[v >>> 4]; + hexChars[j * 2 + 1] = HEXES_SMALL[v & 0x0F]; + } + return new String(hexChars); + } + + private static int fromHexShort(char a) { + if (a >= '0' && a <= '9') { + return a - '0'; + } + if (a >= 'a' && a <= 'f') { + return 10 + (a - 'a'); + } + + throw new RuntimeException(); + } + + public static byte[] fromHex(String hex) { + byte[] res = new byte[hex.length() / 2]; + for (int i = 0; i < res.length; i++) { + res[i] = (byte) ((fromHexShort(hex.charAt(i * 2)) << 4) + fromHexShort(hex.charAt(i * 2 + 1))); + } + return res; + } +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/Main.java b/actor-keygen/src/main/java/im/actor/keygen/Main.java new file mode 100644 index 0000000000..8c9b0a5089 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/Main.java @@ -0,0 +1,31 @@ +package im.actor.keygen; + +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.security.DigestException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Base64; + +public class Main { + public static void main(String[] args) throws DigestException, NoSuchAlgorithmException, IOException { + SecureRandom secureRandom = new SecureRandom(); + if (!new File("keys").exists()) { + new File("keys").mkdir(); + } + for (int i = 0; i < 4; i++) { + File pubFile = new File("keys/actor-key-" + i + ".pub"); + File keyFile = new File("keys/actor-key-" + i + ".key"); + if (pubFile.exists() && keyFile.exists()) { + System.out.println("Key #" + i + " exists. Skipping..."); + continue; + } + Curve25519KeyPair keyPair = Curve25519.keyGen(secureRandom.generateSeed(64)); + FileUtils.writeByteArrayToFile(pubFile, keyPair.getPublicKey()); + FileUtils.writeByteArrayToFile(keyFile, keyPair.getPrivateKey()); + } + System.out.println("Shared Secret: " + Base64.getEncoder().encodeToString(secureRandom.generateSeed(64))); + } +} \ No newline at end of file diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/Arrays.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/Arrays.java new file mode 100755 index 0000000000..9441c32195 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/Arrays.java @@ -0,0 +1,21 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class Arrays { + /** + * Assigns the specified byte value to each element of the specified array + * of bytes. + * + * @param a the array to be filled + * @param val the value to be stored in all elements of the array + */ + public static void fill(byte[] a, byte val) { + for (int i = 0, len = a.length; i < len; i++) + a[i] = val; + } +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/Sha512.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/Sha512.java new file mode 100755 index 0000000000..f437c27b88 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/Sha512.java @@ -0,0 +1,13 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public interface Sha512 { + + void calculateDigest(byte[] out, byte[] in, long length); + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/crypto_verify_32.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/crypto_verify_32.java new file mode 100755 index 0000000000..537ef28e40 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/crypto_verify_32.java @@ -0,0 +1,18 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class crypto_verify_32 { + + public static int crypto_verify_32(byte[] x, byte[] y) { + int differentbits = 0; + for (int count = 0; count < 32; count++) { + differentbits |= (x[count] ^ y[count]); + } + return differentbits; + } +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/curve_sigs.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/curve_sigs.java new file mode 100755 index 0000000000..5a12dc7a91 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/curve_sigs.java @@ -0,0 +1,116 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class curve_sigs { + + public static void curve25519_keygen(byte[] curve25519_pubkey_out, + byte[] curve25519_privkey_in) { + ge_p3 ed = new ge_p3(); /* Ed25519 pubkey point */ + int[] ed_y = new int[10]; + int[] ed_y_plus_one = new int[10]; + int[] one_minus_ed_y = new int[10]; + int[] inv_one_minus_ed_y = new int[10]; + int[] mont_x = new int[10]; + + /* Perform a fixed-base multiplication of the Edwards base point, + (which is efficient due to precalculated tables), then convert + to the Curve25519 montgomery-format public key. In particular, + convert Curve25519's "montgomery" x-coordinate into an Ed25519 + "edwards" y-coordinate: + + mont_x = (ed_y + 1) / (1 - ed_y) + + with projective coordinates: + + mont_x = (ed_y + ed_z) / (ed_z - ed_y) + + NOTE: ed_y=1 is converted to mont_x=0 since fe_invert is mod-exp + */ + + ge_scalarmult_base.ge_scalarmult_base(ed, curve25519_privkey_in); + fe_add.fe_add(ed_y_plus_one, ed.Y, ed.Z); + fe_sub.fe_sub(one_minus_ed_y, ed.Z, ed.Y); + fe_invert.fe_invert(inv_one_minus_ed_y, one_minus_ed_y); + fe_mul.fe_mul(mont_x, ed_y_plus_one, inv_one_minus_ed_y); + fe_tobytes.fe_tobytes(curve25519_pubkey_out, mont_x); + } + + public static int curve25519_sign(Sha512 sha512provider, byte[] signature_out, + byte[] curve25519_privkey, + byte[] msg, int msg_len, + byte[] random) { + ge_p3 ed_pubkey_point = new ge_p3(); /* Ed25519 pubkey point */ + byte[] ed_pubkey = new byte[32]; /* Ed25519 encoded pubkey */ + byte[] sigbuf = new byte[msg_len + 128]; /* working buffer */ + byte sign_bit = 0; + + /* Convert the Curve25519 privkey to an Ed25519 public key */ + ge_scalarmult_base.ge_scalarmult_base(ed_pubkey_point, curve25519_privkey); + ge_p3_tobytes.ge_p3_tobytes(ed_pubkey, ed_pubkey_point); + sign_bit = (byte) (ed_pubkey[31] & 0x80); + + /* Perform an Ed25519 signature with explicit private key */ + sign_modified.crypto_sign_modified(sha512provider, sigbuf, msg, msg_len, curve25519_privkey, + ed_pubkey, random); + System.arraycopy(sigbuf, 0, signature_out, 0, 64); + + /* Encode the sign bit into signature (in unused high bit of S) */ + signature_out[63] &= 0x7F; /* bit should be zero already, but just in case */ + signature_out[63] |= sign_bit; + return 0; + } + + public static int curve25519_verify(Sha512 sha512provider, byte[] signature, + byte[] curve25519_pubkey, + byte[] msg, int msg_len) { + int[] mont_x = new int[10]; + int[] mont_x_minus_one = new int[10]; + int[] mont_x_plus_one = new int[10]; + int[] inv_mont_x_plus_one = new int[10]; + int[] one = new int[10]; + int[] ed_y = new int[10]; + byte[] ed_pubkey = new byte[32]; + long some_retval = 0; + byte[] verifybuf = new byte[msg_len + 64]; /* working buffer */ + byte[] verifybuf2 = new byte[msg_len + 64]; /* working buffer #2 */ + + /* Convert the Curve25519 public key into an Ed25519 public key. In + particular, convert Curve25519's "montgomery" x-coordinate into an + Ed25519 "edwards" y-coordinate: + + ed_y = (mont_x - 1) / (mont_x + 1) + + NOTE: mont_x=-1 is converted to ed_y=0 since fe_invert is mod-exp + + Then move the sign bit into the pubkey from the signature. + */ + fe_frombytes.fe_frombytes(mont_x, curve25519_pubkey); + fe_1.fe_1(one); + fe_sub.fe_sub(mont_x_minus_one, mont_x, one); + fe_add.fe_add(mont_x_plus_one, mont_x, one); + fe_invert.fe_invert(inv_mont_x_plus_one, mont_x_plus_one); + fe_mul.fe_mul(ed_y, mont_x_minus_one, inv_mont_x_plus_one); + fe_tobytes.fe_tobytes(ed_pubkey, ed_y); + + /* Copy the sign bit, and remove it from signature */ + ed_pubkey[31] &= 0x7F; /* bit should be zero already, but just in case */ + ed_pubkey[31] |= (signature[63] & 0x80); + System.arraycopy(signature, 0, verifybuf, 0, 64); + verifybuf[63] &= 0x7F; + + System.arraycopy(msg, 0, verifybuf, 64, (int) msg_len); + + /* Then perform a normal Ed25519 verification, return 0 on success */ + /* The below call has a strange API: */ + /* verifybuf = R || S || message */ + /* verifybuf2 = java to next call gets a copy of verifybuf, S gets + replaced with pubkey for hashing, then the whole thing gets zeroized + (if bad sig), or contains a copy of msg (good sig) */ + return open.crypto_sign_open(sha512provider, verifybuf2, some_retval, verifybuf, 64 + msg_len, ed_pubkey); + } +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_0.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_0.java new file mode 100755 index 0000000000..6f56360f69 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_0.java @@ -0,0 +1,32 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class fe_0 { + +//CONVERT #include "fe.h" + +/* +h = 0 +*/ + +public static void fe_0(int[] h) +{ + h[0] = 0; + h[1] = 0; + h[2] = 0; + h[3] = 0; + h[4] = 0; + h[5] = 0; + h[6] = 0; + h[7] = 0; + h[8] = 0; + h[9] = 0; +} + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_1.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_1.java new file mode 100755 index 0000000000..f472c79090 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_1.java @@ -0,0 +1,30 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class fe_1 { + + //CONVERT #include "fe.h" + + /* + h = 1 + */ + public static void fe_1(int[] h) { + h[0] = 1; + h[1] = 0; + h[2] = 0; + h[3] = 0; + h[4] = 0; + h[5] = 0; + h[6] = 0; + h[7] = 0; + h[8] = 0; + h[9] = 0; + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_add.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_add.java new file mode 100755 index 0000000000..e7b49b8db8 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_add.java @@ -0,0 +1,69 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class fe_add { + +//CONVERT #include "fe.h" + +/* +h = f + g +Can overlap h with f or g. + +Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + +Postconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. +*/ + + public static void fe_add(int[] h, int[] f, int[] g) { + int f0 = f[0]; + int f1 = f[1]; + int f2 = f[2]; + int f3 = f[3]; + int f4 = f[4]; + int f5 = f[5]; + int f6 = f[6]; + int f7 = f[7]; + int f8 = f[8]; + int f9 = f[9]; + int g0 = g[0]; + int g1 = g[1]; + int g2 = g[2]; + int g3 = g[3]; + int g4 = g[4]; + int g5 = g[5]; + int g6 = g[6]; + int g7 = g[7]; + int g8 = g[8]; + int g9 = g[9]; + int h0 = f0 + g0; + int h1 = f1 + g1; + int h2 = f2 + g2; + int h3 = f3 + g3; + int h4 = f4 + g4; + int h5 = f5 + g5; + int h6 = f6 + g6; + int h7 = f7 + g7; + int h8 = f8 + g8; + int h9 = f9 + g9; + h[0] = (int) h0; + h[1] = (int) h1; + h[2] = (int) h2; + h[3] = (int) h3; + h[4] = (int) h4; + h[5] = (int) h5; + h[6] = (int) h6; + h[7] = (int) h7; + h[8] = (int) h8; + h[9] = (int) h9; + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_cmov.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_cmov.java new file mode 100755 index 0000000000..195de36204 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_cmov.java @@ -0,0 +1,75 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class fe_cmov { + + //CONVERT #include "fe.h" + + /* + Replace (f,g) with (g,g) if b == 1; + replace (f,g) with (f,g) if b == 0. + + Preconditions: b in {0,1}. + */ + + public static void fe_cmov(int[] f, int[] g, int b) { + int f0 = f[0]; + int f1 = f[1]; + int f2 = f[2]; + int f3 = f[3]; + int f4 = f[4]; + int f5 = f[5]; + int f6 = f[6]; + int f7 = f[7]; + int f8 = f[8]; + int f9 = f[9]; + int g0 = g[0]; + int g1 = g[1]; + int g2 = g[2]; + int g3 = g[3]; + int g4 = g[4]; + int g5 = g[5]; + int g6 = g[6]; + int g7 = g[7]; + int g8 = g[8]; + int g9 = g[9]; + int x0 = f0 ^ g0; + int x1 = f1 ^ g1; + int x2 = f2 ^ g2; + int x3 = f3 ^ g3; + int x4 = f4 ^ g4; + int x5 = f5 ^ g5; + int x6 = f6 ^ g6; + int x7 = f7 ^ g7; + int x8 = f8 ^ g8; + int x9 = f9 ^ g9; + b = -b; + x0 &= b; + x1 &= b; + x2 &= b; + x3 &= b; + x4 &= b; + x5 &= b; + x6 &= b; + x7 &= b; + x8 &= b; + x9 &= b; + f[0] = f0 ^ x0; + f[1] = f1 ^ x1; + f[2] = f2 ^ x2; + f[3] = f3 ^ x3; + f[4] = f4 ^ x4; + f[5] = f5 ^ x5; + f[6] = f6 ^ x6; + f[7] = f7 ^ x7; + f[8] = f8 ^ x8; + f[9] = f9 ^ x9; + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_copy.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_copy.java new file mode 100755 index 0000000000..a569c93db5 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_copy.java @@ -0,0 +1,40 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class fe_copy { + +//CONVERT #include "fe.h" + + /* + h = f + */ + public static void fe_copy(int[] h, int[] f) { + int f0 = f[0]; + int f1 = f[1]; + int f2 = f[2]; + int f3 = f[3]; + int f4 = f[4]; + int f5 = f[5]; + int f6 = f[6]; + int f7 = f[7]; + int f8 = f[8]; + int f9 = f[9]; + h[0] = f0; + h[1] = f1; + h[2] = f2; + h[3] = f3; + h[4] = f4; + h[5] = f5; + h[6] = f6; + h[7] = f7; + h[8] = f8; + h[9] = f9; + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_cswap.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_cswap.java new file mode 100755 index 0000000000..e1bb8a793d --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_cswap.java @@ -0,0 +1,86 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class fe_cswap { + +//CONVERT #include +//CONVERT #include "fe.h" + +/* +Replace (f,g) with (g,f) if b == 1; +replace (f,g) with (f,g) if b == 0. + +Preconditions: b in {0,1}. +*/ + + public static void fe_cswap(int[] f, int[] g, int b) { + int f0 = f[0]; + int f1 = f[1]; + int f2 = f[2]; + int f3 = f[3]; + int f4 = f[4]; + int f5 = f[5]; + int f6 = f[6]; + int f7 = f[7]; + int f8 = f[8]; + int f9 = f[9]; + int g0 = g[0]; + int g1 = g[1]; + int g2 = g[2]; + int g3 = g[3]; + int g4 = g[4]; + int g5 = g[5]; + int g6 = g[6]; + int g7 = g[7]; + int g8 = g[8]; + int g9 = g[9]; + int x0 = f0 ^ g0; + int x1 = f1 ^ g1; + int x2 = f2 ^ g2; + int x3 = f3 ^ g3; + int x4 = f4 ^ g4; + int x5 = f5 ^ g5; + int x6 = f6 ^ g6; + int x7 = f7 ^ g7; + int x8 = f8 ^ g8; + int x9 = f9 ^ g9; + b = -b; + x0 &= b; + x1 &= b; + x2 &= b; + x3 &= b; + x4 &= b; + x5 &= b; + x6 &= b; + x7 &= b; + x8 &= b; + x9 &= b; + f[0] = f0 ^ x0; + f[1] = f1 ^ x1; + f[2] = f2 ^ x2; + f[3] = f3 ^ x3; + f[4] = f4 ^ x4; + f[5] = f5 ^ x5; + f[6] = f6 ^ x6; + f[7] = f7 ^ x7; + f[8] = f8 ^ x8; + f[9] = f9 ^ x9; + g[0] = g0 ^ x0; + g[1] = g1 ^ x1; + g[2] = g2 ^ x2; + g[3] = g3 ^ x3; + g[4] = g4 ^ x4; + g[5] = g5 ^ x5; + g[6] = g6 ^ x6; + g[7] = g7 ^ x7; + g[8] = g8 ^ x8; + g[9] = g9 ^ x9; + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_frombytes.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_frombytes.java new file mode 100755 index 0000000000..7785c1f30e --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_frombytes.java @@ -0,0 +1,103 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class fe_frombytes { + +//CONVERT #include "fe.h" +//CONVERT #include "long.h" +//CONVERT #include "long.h" + + public static long load_3(byte[] in, int index) { + long result; + result = ((long) in[index + 0]) & 0xFF; + result |= (((long) in[index + 1]) << 8) & 0xFF00; + result |= (((long) in[index + 2]) << 16) & 0xFF0000; + return result; + } + + public static long load_4(byte[] in, int index) { + long result; + result = (((long) in[index + 0]) & 0xFF); + result |= ((((long) in[index + 1]) << 8) & 0xFF00); + result |= ((((long) in[index + 2]) << 16) & 0xFF0000); + result |= ((((long) in[index + 3]) << 24) & 0xFF000000L); + return result; + } + +/* +Ignores top bit of h. +*/ + + public static void fe_frombytes(int[] h, byte[] s) { + long h0 = load_4(s, 0); + long h1 = load_3(s, 4) << 6; + long h2 = load_3(s, 7) << 5; + long h3 = load_3(s, 10) << 3; + long h4 = load_3(s, 13) << 2; + long h5 = load_4(s, 16); + long h6 = load_3(s, 20) << 7; + long h7 = load_3(s, 23) << 5; + long h8 = load_3(s, 26) << 4; + long h9 = (load_3(s, 29) & 8388607) << 2; + long carry0; + long carry1; + long carry2; + long carry3; + long carry4; + long carry5; + long carry6; + long carry7; + long carry8; + long carry9; + + carry9 = (h9 + (long) (1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 << 25; + carry1 = (h1 + (long) (1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry3 = (h3 + (long) (1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry5 = (h5 + (long) (1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 << 25; + carry7 = (h7 + (long) (1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 << 25; + + carry0 = (h0 + (long) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry2 = (h2 + (long) (1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry4 = (h4 + (long) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry6 = (h6 + (long) (1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 << 26; + carry8 = (h8 + (long) (1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 << 26; + + h[0] = (int) h0; + h[1] = (int) h1; + h[2] = (int) h2; + h[3] = (int) h3; + h[4] = (int) h4; + h[5] = (int) h5; + h[6] = (int) h6; + h[7] = (int) h7; + h[8] = (int) h8; + h[9] = (int) h9; + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_invert.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_invert.java new file mode 100755 index 0000000000..1c84670b72 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_invert.java @@ -0,0 +1,197 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class fe_invert { + +//CONVERT #include "fe.h" + + public static void fe_invert(int[] out, int[] z) { + int[] t0 = new int[10]; + int[] t1 = new int[10]; + int[] t2 = new int[10]; + int[] t3 = new int[10]; + int i; + +//CONVERT #include "pow225521.h" + +/* qhasm: fe z1 */ + +/* qhasm: fe z2 */ + +/* qhasm: fe z8 */ + +/* qhasm: fe z9 */ + +/* qhasm: fe z11 */ + +/* qhasm: fe z22 */ + +/* qhasm: fe z_5_0 */ + +/* qhasm: fe z_10_5 */ + +/* qhasm: fe z_10_0 */ + +/* qhasm: fe z_20_10 */ + +/* qhasm: fe z_20_0 */ + +/* qhasm: fe z_40_20 */ + +/* qhasm: fe z_40_0 */ + +/* qhasm: fe z_50_10 */ + +/* qhasm: fe z_50_0 */ + +/* qhasm: fe z_100_50 */ + +/* qhasm: fe z_100_0 */ + +/* qhasm: fe z_200_100 */ + +/* qhasm: fe z_200_0 */ + +/* qhasm: fe z_250_50 */ + +/* qhasm: fe z_250_0 */ + +/* qhasm: fe z_255_5 */ + +/* qhasm: fe z_255_21 */ + +/* qhasm: enter pow225521 */ + +/* qhasm: z2 = z1^2^1 */ +/* asm 1: fe_sq.fe_sq(>z2=fe#1,z2=fe#1,>z2=fe#1); */ +/* asm 2: fe_sq.fe_sq(>z2=t0,z2=t0,>z2=t0); */ + fe_sq.fe_sq(t0, z); + for (i = 1; i < 1; ++i) fe_sq.fe_sq(t0, t0); + +/* qhasm: z8 = z2^2^2 */ +/* asm 1: fe_sq.fe_sq(>z8=fe#2,z8=fe#2,>z8=fe#2); */ +/* asm 2: fe_sq.fe_sq(>z8=t1,z8=t1,>z8=t1); */ + fe_sq.fe_sq(t1, t0); + for (i = 1; i < 2; ++i) fe_sq.fe_sq(t1, t1); + +/* qhasm: z9 = z1*z8 */ +/* asm 1: fe_mul.fe_mul(>z9=fe#2,z9=t1,z11=fe#1,z11=t0,z22=fe#3,z22=fe#3,>z22=fe#3); */ +/* asm 2: fe_sq.fe_sq(>z22=t2,z22=t2,>z22=t2); */ + fe_sq.fe_sq(t2, t0); + for (i = 1; i < 1; ++i) fe_sq.fe_sq(t2, t2); + +/* qhasm: z_5_0 = z9*z22 */ +/* asm 1: fe_mul.fe_mul(>z_5_0=fe#2,z_5_0=t1,z_10_5=fe#3,z_10_5=fe#3,>z_10_5=fe#3); */ +/* asm 2: fe_sq.fe_sq(>z_10_5=t2,z_10_5=t2,>z_10_5=t2); */ + fe_sq.fe_sq(t2, t1); + for (i = 1; i < 5; ++i) fe_sq.fe_sq(t2, t2); + +/* qhasm: z_10_0 = z_10_5*z_5_0 */ +/* asm 1: fe_mul.fe_mul(>z_10_0=fe#2,z_10_0=t1,z_20_10=fe#3,z_20_10=fe#3,>z_20_10=fe#3); */ +/* asm 2: fe_sq.fe_sq(>z_20_10=t2,z_20_10=t2,>z_20_10=t2); */ + fe_sq.fe_sq(t2, t1); + for (i = 1; i < 10; ++i) fe_sq.fe_sq(t2, t2); + +/* qhasm: z_20_0 = z_20_10*z_10_0 */ +/* asm 1: fe_mul.fe_mul(>z_20_0=fe#3,z_20_0=t2,z_40_20=fe#4,z_40_20=fe#4,>z_40_20=fe#4); */ +/* asm 2: fe_sq.fe_sq(>z_40_20=t3,z_40_20=t3,>z_40_20=t3); */ + fe_sq.fe_sq(t3, t2); + for (i = 1; i < 20; ++i) fe_sq.fe_sq(t3, t3); + +/* qhasm: z_40_0 = z_40_20*z_20_0 */ +/* asm 1: fe_mul.fe_mul(>z_40_0=fe#3,z_40_0=t2,z_50_10=fe#3,z_50_10=fe#3,>z_50_10=fe#3); */ +/* asm 2: fe_sq.fe_sq(>z_50_10=t2,z_50_10=t2,>z_50_10=t2); */ + fe_sq.fe_sq(t2, t2); + for (i = 1; i < 10; ++i) fe_sq.fe_sq(t2, t2); + +/* qhasm: z_50_0 = z_50_10*z_10_0 */ +/* asm 1: fe_mul.fe_mul(>z_50_0=fe#2,z_50_0=t1,z_100_50=fe#3,z_100_50=fe#3,>z_100_50=fe#3); */ +/* asm 2: fe_sq.fe_sq(>z_100_50=t2,z_100_50=t2,>z_100_50=t2); */ + fe_sq.fe_sq(t2, t1); + for (i = 1; i < 50; ++i) fe_sq.fe_sq(t2, t2); + +/* qhasm: z_100_0 = z_100_50*z_50_0 */ +/* asm 1: fe_mul.fe_mul(>z_100_0=fe#3,z_100_0=t2,z_200_100=fe#4,z_200_100=fe#4,>z_200_100=fe#4); */ +/* asm 2: fe_sq.fe_sq(>z_200_100=t3,z_200_100=t3,>z_200_100=t3); */ + fe_sq.fe_sq(t3, t2); + for (i = 1; i < 100; ++i) fe_sq.fe_sq(t3, t3); + +/* qhasm: z_200_0 = z_200_100*z_100_0 */ +/* asm 1: fe_mul.fe_mul(>z_200_0=fe#3,z_200_0=t2,z_250_50=fe#3,z_250_50=fe#3,>z_250_50=fe#3); */ +/* asm 2: fe_sq.fe_sq(>z_250_50=t2,z_250_50=t2,>z_250_50=t2); */ + fe_sq.fe_sq(t2, t2); + for (i = 1; i < 50; ++i) fe_sq.fe_sq(t2, t2); + +/* qhasm: z_250_0 = z_250_50*z_50_0 */ +/* asm 1: fe_mul.fe_mul(>z_250_0=fe#2,z_250_0=t1,z_255_5=fe#2,z_255_5=fe#2,>z_255_5=fe#2); */ +/* asm 2: fe_sq.fe_sq(>z_255_5=t1,z_255_5=t1,>z_255_5=t1); */ + fe_sq.fe_sq(t1, t1); + for (i = 1; i < 5; ++i) fe_sq.fe_sq(t1, t1); + +/* qhasm: z_255_21 = z_255_5*z11 */ +/* asm 1: fe_mul.fe_mul(>z_255_21=fe#12,z_255_21=out,> 26; + hr[1] += carry0; + hr[0] -= carry0 << 26; + carry4 = (hr[4] + (long) (1 << 25)) >> 26; + hr[5] += carry4; + hr[4] -= carry4 << 26; + /* |h0| <= 2^25 */ + /* |h4| <= 2^25 */ + /* |h1| <= 1.71*2^59 */ + /* |h5| <= 1.71*2^59 */ + + carry1 = (hr[1] + (long) (1 << 24)) >> 25; + hr[2] += carry1; + hr[1] -= carry1 << 25; + carry5 = (hr[5] + (long) (1 << 24)) >> 25; + hr[6] += carry5; + hr[5] -= carry5 << 25; + /* |h1| <= 2^24; from now on fits into int32 */ + /* |h5| <= 2^24; from now on fits into int32 */ + /* |h2| <= 1.41*2^60 */ + /* |h6| <= 1.41*2^60 */ + + carry2 = (hr[2] + (long) (1 << 25)) >> 26; + hr[3] += carry2; + hr[2] -= carry2 << 26; + carry6 = (hr[6] + (long) (1 << 25)) >> 26; + hr[7] += carry6; + hr[6] -= carry6 << 26; + /* |h2| <= 2^25; from now on fits into int32 unchanged */ + /* |h6| <= 2^25; from now on fits into int32 unchanged */ + /* |h3| <= 1.71*2^59 */ + /* |h7| <= 1.71*2^59 */ + + carry3 = (hr[3] + (long) (1 << 24)) >> 25; + hr[4] += carry3; + hr[3] -= carry3 << 25; + carry7 = (hr[7] + (long) (1 << 24)) >> 25; + hr[8] += carry7; + hr[7] -= carry7 << 25; + /* |h3| <= 2^24; from now on fits into int32 unchanged */ + /* |h7| <= 2^24; from now on fits into int32 unchanged */ + /* |h4| <= 1.72*2^34 */ + /* |h8| <= 1.41*2^60 */ + + carry4 = (hr[4] + (long) (1 << 25)) >> 26; + hr[5] += carry4; + hr[4] -= carry4 << 26; + carry8 = (hr[8] + (long) (1 << 25)) >> 26; + hr[9] += carry8; + hr[8] -= carry8 << 26; + /* |h4| <= 2^25; from now on fits into int32 unchanged */ + /* |h8| <= 2^25; from now on fits into int32 unchanged */ + /* |h5| <= 1.01*2^24 */ + /* |h9| <= 1.71*2^59 */ + + carry9 = (hr[9] + (long) (1 << 24)) >> 25; + hr[0] += carry9 * 19; + hr[9] -= carry9 << 25; + /* |h9| <= 2^24; from now on fits into int32 unchanged */ + /* |h0| <= 1.1*2^39 */ + + carry0 = (hr[0] + (long) (1 << 25)) >> 26; + hr[1] += carry0; + hr[0] -= carry0 << 26; + /* |h0| <= 2^25; from now on fits into int32 unchanged */ + /* |h1| <= 1.01*2^24 */ + + h[0] = (int) hr[0]; + h[1] = (int) hr[1]; + h[2] = (int) hr[2]; + h[3] = (int) hr[3]; + h[4] = (int) hr[4]; + h[5] = (int) hr[5]; + h[6] = (int) hr[6]; + h[7] = (int) hr[7]; + h[8] = (int) hr[8]; + h[9] = (int) hr[9]; + } +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_mul121666.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_mul121666.java new file mode 100755 index 0000000000..18a83baa3b --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_mul121666.java @@ -0,0 +1,102 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class fe_mul121666 { + +//CONVERT #include "fe.h" +//CONVERT #include "long.h" + +/* +h = f * 121666 +Can overlap h with f. + +Preconditions: + |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + +Postconditions: + |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. +*/ + + public static void fe_mul121666(int[] h, int[] f) { + int f0 = f[0]; + int f1 = f[1]; + int f2 = f[2]; + int f3 = f[3]; + int f4 = f[4]; + int f5 = f[5]; + int f6 = f[6]; + int f7 = f[7]; + int f8 = f[8]; + int f9 = f[9]; + long h0 = f0 * (long) 121666; + long h1 = f1 * (long) 121666; + long h2 = f2 * (long) 121666; + long h3 = f3 * (long) 121666; + long h4 = f4 * (long) 121666; + long h5 = f5 * (long) 121666; + long h6 = f6 * (long) 121666; + long h7 = f7 * (long) 121666; + long h8 = f8 * (long) 121666; + long h9 = f9 * (long) 121666; + long carry0; + long carry1; + long carry2; + long carry3; + long carry4; + long carry5; + long carry6; + long carry7; + long carry8; + long carry9; + + carry9 = (h9 + (long) (1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 << 25; + carry1 = (h1 + (long) (1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry3 = (h3 + (long) (1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry5 = (h5 + (long) (1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 << 25; + carry7 = (h7 + (long) (1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 << 25; + + carry0 = (h0 + (long) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry2 = (h2 + (long) (1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry4 = (h4 + (long) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry6 = (h6 + (long) (1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 << 26; + carry8 = (h8 + (long) (1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 << 26; + + h[0] = (int) h0; + h[1] = (int) h1; + h[2] = (int) h2; + h[3] = (int) h3; + h[4] = (int) h4; + h[5] = (int) h5; + h[6] = (int) h6; + h[7] = (int) h7; + h[8] = (int) h8; + h[9] = (int) h9; + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_neg.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_neg.java new file mode 100755 index 0000000000..c9a3b92a88 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_neg.java @@ -0,0 +1,57 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class fe_neg { + +//CONVERT #include "fe.h" + +/* +h = -f + +Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + +Postconditions: + |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. +*/ + + public static void fe_neg(int[] h, int[] f) { + int f0 = f[0]; + int f1 = f[1]; + int f2 = f[2]; + int f3 = f[3]; + int f4 = f[4]; + int f5 = f[5]; + int f6 = f[6]; + int f7 = f[7]; + int f8 = f[8]; + int f9 = f[9]; + int h0 = -f0; + int h1 = -f1; + int h2 = -f2; + int h3 = -f3; + int h4 = -f4; + int h5 = -f5; + int h6 = -f6; + int h7 = -f7; + int h8 = -f8; + int h9 = -f9; + h[0] = (int) h0; + h[1] = (int) h1; + h[2] = (int) h2; + h[3] = (int) h3; + h[4] = (int) h4; + h[5] = (int) h5; + h[6] = (int) h6; + h[7] = (int) h7; + h[8] = (int) h8; + h[9] = (int) h9; + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_pow22523.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_pow22523.java new file mode 100755 index 0000000000..2d580c4cd3 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_pow22523.java @@ -0,0 +1,196 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class fe_pow22523 { + +//CONVERT #include "fe.h" + + public static void fe_pow22523(int[] out, int[] z) { + int[] t0 = new int[10]; + int[] t1 = new int[10]; + int[] t2 = new int[10]; + int i; + +//CONVERT #include "pow22523.h" + +/* qhasm: fe z1 */ + +/* qhasm: fe z2 */ + +/* qhasm: fe z8 */ + +/* qhasm: fe z9 */ + +/* qhasm: fe z11 */ + +/* qhasm: fe z22 */ + +/* qhasm: fe z_5_0 */ + +/* qhasm: fe z_10_5 */ + +/* qhasm: fe z_10_0 */ + +/* qhasm: fe z_20_10 */ + +/* qhasm: fe z_20_0 */ + +/* qhasm: fe z_40_20 */ + +/* qhasm: fe z_40_0 */ + +/* qhasm: fe z_50_10 */ + +/* qhasm: fe z_50_0 */ + +/* qhasm: fe z_100_50 */ + +/* qhasm: fe z_100_0 */ + +/* qhasm: fe z_200_100 */ + +/* qhasm: fe z_200_0 */ + +/* qhasm: fe z_250_50 */ + +/* qhasm: fe z_250_0 */ + +/* qhasm: fe z_252_2 */ + +/* qhasm: fe z_252_3 */ + +/* qhasm: enter pow22523 */ + +/* qhasm: z2 = z1^2^1 */ +/* asm 1: fe_sq.fe_sq(>z2=fe#1,z2=fe#1,>z2=fe#1); */ +/* asm 2: fe_sq.fe_sq(>z2=t0,z2=t0,>z2=t0); */ + fe_sq.fe_sq(t0, z); + for (i = 1; i < 1; ++i) fe_sq.fe_sq(t0, t0); + +/* qhasm: z8 = z2^2^2 */ +/* asm 1: fe_sq.fe_sq(>z8=fe#2,z8=fe#2,>z8=fe#2); */ +/* asm 2: fe_sq.fe_sq(>z8=t1,z8=t1,>z8=t1); */ + fe_sq.fe_sq(t1, t0); + for (i = 1; i < 2; ++i) fe_sq.fe_sq(t1, t1); + +/* qhasm: z9 = z1*z8 */ +/* asm 1: fe_mul.fe_mul(>z9=fe#2,z9=t1,z11=fe#1,z11=t0,z22=fe#1,z22=fe#1,>z22=fe#1); */ +/* asm 2: fe_sq.fe_sq(>z22=t0,z22=t0,>z22=t0); */ + fe_sq.fe_sq(t0, t0); + for (i = 1; i < 1; ++i) fe_sq.fe_sq(t0, t0); + +/* qhasm: z_5_0 = z9*z22 */ +/* asm 1: fe_mul.fe_mul(>z_5_0=fe#1,z_5_0=t0,z_10_5=fe#2,z_10_5=fe#2,>z_10_5=fe#2); */ +/* asm 2: fe_sq.fe_sq(>z_10_5=t1,z_10_5=t1,>z_10_5=t1); */ + fe_sq.fe_sq(t1, t0); + for (i = 1; i < 5; ++i) fe_sq.fe_sq(t1, t1); + +/* qhasm: z_10_0 = z_10_5*z_5_0 */ +/* asm 1: fe_mul.fe_mul(>z_10_0=fe#1,z_10_0=t0,z_20_10=fe#2,z_20_10=fe#2,>z_20_10=fe#2); */ +/* asm 2: fe_sq.fe_sq(>z_20_10=t1,z_20_10=t1,>z_20_10=t1); */ + fe_sq.fe_sq(t1, t0); + for (i = 1; i < 10; ++i) fe_sq.fe_sq(t1, t1); + +/* qhasm: z_20_0 = z_20_10*z_10_0 */ +/* asm 1: fe_mul.fe_mul(>z_20_0=fe#2,z_20_0=t1,z_40_20=fe#3,z_40_20=fe#3,>z_40_20=fe#3); */ +/* asm 2: fe_sq.fe_sq(>z_40_20=t2,z_40_20=t2,>z_40_20=t2); */ + fe_sq.fe_sq(t2, t1); + for (i = 1; i < 20; ++i) fe_sq.fe_sq(t2, t2); + +/* qhasm: z_40_0 = z_40_20*z_20_0 */ +/* asm 1: fe_mul.fe_mul(>z_40_0=fe#2,z_40_0=t1,z_50_10=fe#2,z_50_10=fe#2,>z_50_10=fe#2); */ +/* asm 2: fe_sq.fe_sq(>z_50_10=t1,z_50_10=t1,>z_50_10=t1); */ + fe_sq.fe_sq(t1, t1); + for (i = 1; i < 10; ++i) fe_sq.fe_sq(t1, t1); + +/* qhasm: z_50_0 = z_50_10*z_10_0 */ +/* asm 1: fe_mul.fe_mul(>z_50_0=fe#1,z_50_0=t0,z_100_50=fe#2,z_100_50=fe#2,>z_100_50=fe#2); */ +/* asm 2: fe_sq.fe_sq(>z_100_50=t1,z_100_50=t1,>z_100_50=t1); */ + fe_sq.fe_sq(t1, t0); + for (i = 1; i < 50; ++i) fe_sq.fe_sq(t1, t1); + +/* qhasm: z_100_0 = z_100_50*z_50_0 */ +/* asm 1: fe_mul.fe_mul(>z_100_0=fe#2,z_100_0=t1,z_200_100=fe#3,z_200_100=fe#3,>z_200_100=fe#3); */ +/* asm 2: fe_sq.fe_sq(>z_200_100=t2,z_200_100=t2,>z_200_100=t2); */ + fe_sq.fe_sq(t2, t1); + for (i = 1; i < 100; ++i) fe_sq.fe_sq(t2, t2); + +/* qhasm: z_200_0 = z_200_100*z_100_0 */ +/* asm 1: fe_mul.fe_mul(>z_200_0=fe#2,z_200_0=t1,z_250_50=fe#2,z_250_50=fe#2,>z_250_50=fe#2); */ +/* asm 2: fe_sq.fe_sq(>z_250_50=t1,z_250_50=t1,>z_250_50=t1); */ + fe_sq.fe_sq(t1, t1); + for (i = 1; i < 50; ++i) fe_sq.fe_sq(t1, t1); + +/* qhasm: z_250_0 = z_250_50*z_50_0 */ +/* asm 1: fe_mul.fe_mul(>z_250_0=fe#1,z_250_0=t0,z_252_2=fe#1,z_252_2=fe#1,>z_252_2=fe#1); */ +/* asm 2: fe_sq.fe_sq(>z_252_2=t0,z_252_2=t0,>z_252_2=t0); */ + fe_sq.fe_sq(t0, t0); + for (i = 1; i < 2; ++i) fe_sq.fe_sq(t0, t0); + +/* qhasm: z_252_3 = z_252_2*z1 */ +/* asm 1: fe_mul.fe_mul(>z_252_3=fe#12,z_252_3=out,> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry4 = (h4 + (long) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + + carry1 = (h1 + (long) (1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry5 = (h5 + (long) (1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 << 25; + + carry2 = (h2 + (long) (1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry6 = (h6 + (long) (1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 << 26; + + carry3 = (h3 + (long) (1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry7 = (h7 + (long) (1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 << 25; + + carry4 = (h4 + (long) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry8 = (h8 + (long) (1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 << 26; + + carry9 = (h9 + (long) (1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 << 25; + + carry0 = (h0 + (long) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + + h[0] = (int) h0; + h[1] = (int) h1; + h[2] = (int) h2; + h[3] = (int) h3; + h[4] = (int) h4; + h[5] = (int) h5; + h[6] = (int) h6; + h[7] = (int) h7; + h[8] = (int) h8; + h[9] = (int) h9; + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_sq2.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_sq2.java new file mode 100755 index 0000000000..0bf6f2a793 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_sq2.java @@ -0,0 +1,196 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class fe_sq2 { + +//CONVERT #include "fe.h" +//CONVERT #include "long.h" + +/* +h = 2 * f * f +Can overlap h with f. + +Preconditions: + |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. + +Postconditions: + |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. +*/ + +/* +See fe_mul.c for discussion of implementation strategy. +*/ + + public static void fe_sq2(int[] h, int[] f) { + int f0 = f[0]; + int f1 = f[1]; + int f2 = f[2]; + int f3 = f[3]; + int f4 = f[4]; + int f5 = f[5]; + int f6 = f[6]; + int f7 = f[7]; + int f8 = f[8]; + int f9 = f[9]; + int f0_2 = 2 * f0; + int f1_2 = 2 * f1; + int f2_2 = 2 * f2; + int f3_2 = 2 * f3; + int f4_2 = 2 * f4; + int f5_2 = 2 * f5; + int f6_2 = 2 * f6; + int f7_2 = 2 * f7; + int f5_38 = 38 * f5; /* 1.959375*2^30 */ + int f6_19 = 19 * f6; /* 1.959375*2^30 */ + int f7_38 = 38 * f7; /* 1.959375*2^30 */ + int f8_19 = 19 * f8; /* 1.959375*2^30 */ + int f9_38 = 38 * f9; /* 1.959375*2^30 */ + long f0f0 = f0 * (long) f0; + long f0f1_2 = f0_2 * (long) f1; + long f0f2_2 = f0_2 * (long) f2; + long f0f3_2 = f0_2 * (long) f3; + long f0f4_2 = f0_2 * (long) f4; + long f0f5_2 = f0_2 * (long) f5; + long f0f6_2 = f0_2 * (long) f6; + long f0f7_2 = f0_2 * (long) f7; + long f0f8_2 = f0_2 * (long) f8; + long f0f9_2 = f0_2 * (long) f9; + long f1f1_2 = f1_2 * (long) f1; + long f1f2_2 = f1_2 * (long) f2; + long f1f3_4 = f1_2 * (long) f3_2; + long f1f4_2 = f1_2 * (long) f4; + long f1f5_4 = f1_2 * (long) f5_2; + long f1f6_2 = f1_2 * (long) f6; + long f1f7_4 = f1_2 * (long) f7_2; + long f1f8_2 = f1_2 * (long) f8; + long f1f9_76 = f1_2 * (long) f9_38; + long f2f2 = f2 * (long) f2; + long f2f3_2 = f2_2 * (long) f3; + long f2f4_2 = f2_2 * (long) f4; + long f2f5_2 = f2_2 * (long) f5; + long f2f6_2 = f2_2 * (long) f6; + long f2f7_2 = f2_2 * (long) f7; + long f2f8_38 = f2_2 * (long) f8_19; + long f2f9_38 = f2 * (long) f9_38; + long f3f3_2 = f3_2 * (long) f3; + long f3f4_2 = f3_2 * (long) f4; + long f3f5_4 = f3_2 * (long) f5_2; + long f3f6_2 = f3_2 * (long) f6; + long f3f7_76 = f3_2 * (long) f7_38; + long f3f8_38 = f3_2 * (long) f8_19; + long f3f9_76 = f3_2 * (long) f9_38; + long f4f4 = f4 * (long) f4; + long f4f5_2 = f4_2 * (long) f5; + long f4f6_38 = f4_2 * (long) f6_19; + long f4f7_38 = f4 * (long) f7_38; + long f4f8_38 = f4_2 * (long) f8_19; + long f4f9_38 = f4 * (long) f9_38; + long f5f5_38 = f5 * (long) f5_38; + long f5f6_38 = f5_2 * (long) f6_19; + long f5f7_76 = f5_2 * (long) f7_38; + long f5f8_38 = f5_2 * (long) f8_19; + long f5f9_76 = f5_2 * (long) f9_38; + long f6f6_19 = f6 * (long) f6_19; + long f6f7_38 = f6 * (long) f7_38; + long f6f8_38 = f6_2 * (long) f8_19; + long f6f9_38 = f6 * (long) f9_38; + long f7f7_38 = f7 * (long) f7_38; + long f7f8_38 = f7_2 * (long) f8_19; + long f7f9_76 = f7_2 * (long) f9_38; + long f8f8_19 = f8 * (long) f8_19; + long f8f9_38 = f8 * (long) f9_38; + long f9f9_38 = f9 * (long) f9_38; + long h0 = f0f0 + f1f9_76 + f2f8_38 + f3f7_76 + f4f6_38 + f5f5_38; + long h1 = f0f1_2 + f2f9_38 + f3f8_38 + f4f7_38 + f5f6_38; + long h2 = f0f2_2 + f1f1_2 + f3f9_76 + f4f8_38 + f5f7_76 + f6f6_19; + long h3 = f0f3_2 + f1f2_2 + f4f9_38 + f5f8_38 + f6f7_38; + long h4 = f0f4_2 + f1f3_4 + f2f2 + f5f9_76 + f6f8_38 + f7f7_38; + long h5 = f0f5_2 + f1f4_2 + f2f3_2 + f6f9_38 + f7f8_38; + long h6 = f0f6_2 + f1f5_4 + f2f4_2 + f3f3_2 + f7f9_76 + f8f8_19; + long h7 = f0f7_2 + f1f6_2 + f2f5_2 + f3f4_2 + f8f9_38; + long h8 = f0f8_2 + f1f7_4 + f2f6_2 + f3f5_4 + f4f4 + f9f9_38; + long h9 = f0f9_2 + f1f8_2 + f2f7_2 + f3f6_2 + f4f5_2; + long carry0; + long carry1; + long carry2; + long carry3; + long carry4; + long carry5; + long carry6; + long carry7; + long carry8; + long carry9; + + h0 += h0; + h1 += h1; + h2 += h2; + h3 += h3; + h4 += h4; + h5 += h5; + h6 += h6; + h7 += h7; + h8 += h8; + h9 += h9; + + carry0 = (h0 + (long) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry4 = (h4 + (long) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + + carry1 = (h1 + (long) (1 << 24)) >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry5 = (h5 + (long) (1 << 24)) >> 25; + h6 += carry5; + h5 -= carry5 << 25; + + carry2 = (h2 + (long) (1 << 25)) >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry6 = (h6 + (long) (1 << 25)) >> 26; + h7 += carry6; + h6 -= carry6 << 26; + + carry3 = (h3 + (long) (1 << 24)) >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry7 = (h7 + (long) (1 << 24)) >> 25; + h8 += carry7; + h7 -= carry7 << 25; + + carry4 = (h4 + (long) (1 << 25)) >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry8 = (h8 + (long) (1 << 25)) >> 26; + h9 += carry8; + h8 -= carry8 << 26; + + carry9 = (h9 + (long) (1 << 24)) >> 25; + h0 += carry9 * 19; + h9 -= carry9 << 25; + + carry0 = (h0 + (long) (1 << 25)) >> 26; + h1 += carry0; + h0 -= carry0 << 26; + + h[0] = (int) h0; + h[1] = (int) h1; + h[2] = (int) h2; + h[3] = (int) h3; + h[4] = (int) h4; + h[5] = (int) h5; + h[6] = (int) h6; + h[7] = (int) h7; + h[8] = (int) h8; + h[9] = (int) h9; + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_sub.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_sub.java new file mode 100755 index 0000000000..bd142607d5 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_sub.java @@ -0,0 +1,69 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class fe_sub { + +//CONVERT #include "fe.h" + +/* +h = f - g +Can overlap h with f or g. + +Preconditions: + |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. + +Postconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. +*/ + + public static void fe_sub(int[] h, int[] f, int[] g) { + int f0 = f[0]; + int f1 = f[1]; + int f2 = f[2]; + int f3 = f[3]; + int f4 = f[4]; + int f5 = f[5]; + int f6 = f[6]; + int f7 = f[7]; + int f8 = f[8]; + int f9 = f[9]; + int g0 = g[0]; + int g1 = g[1]; + int g2 = g[2]; + int g3 = g[3]; + int g4 = g[4]; + int g5 = g[5]; + int g6 = g[6]; + int g7 = g[7]; + int g8 = g[8]; + int g9 = g[9]; + int h0 = f0 - g0; + int h1 = f1 - g1; + int h2 = f2 - g2; + int h3 = f3 - g3; + int h4 = f4 - g4; + int h5 = f5 - g5; + int h6 = f6 - g6; + int h7 = f7 - g7; + int h8 = f8 - g8; + int h9 = f9 - g9; + h[0] = (int) h0; + h[1] = (int) h1; + h[2] = (int) h2; + h[3] = (int) h3; + h[4] = (int) h4; + h[5] = (int) h5; + h[6] = (int) h6; + h[7] = (int) h7; + h[8] = (int) h8; + h[9] = (int) h9; + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_tobytes.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_tobytes.java new file mode 100755 index 0000000000..52ac449393 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/fe_tobytes.java @@ -0,0 +1,150 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class fe_tobytes { + +//CONVERT #include "fe.h" + +/* +Preconditions: + |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. + +Write p=2^255-19; q=floor(h/p). +Basic claim: q = floor(2^(-255)(h + 19 2^(-25)h9 + 2^(-1))). + +Proof: + Have |h|<=p so |q|<=1 so |19^2 2^(-255) q|<1/4. + Also have |h-2^230 h9|<2^231 so |19 2^(-255)(h-2^230 h9)|<1/4. + + Write y=2^(-1)-19^2 2^(-255)q-19 2^(-255)(h-2^230 h9). + Then 0> 25; + q = (h0 + q) >> 26; + q = (h1 + q) >> 25; + q = (h2 + q) >> 26; + q = (h3 + q) >> 25; + q = (h4 + q) >> 26; + q = (h5 + q) >> 25; + q = (h6 + q) >> 26; + q = (h7 + q) >> 25; + q = (h8 + q) >> 26; + q = (h9 + q) >> 25; + + /* Goal: Output h-(2^255-19)q, which is between 0 and 2^255-20. */ + h0 += 19 * q; + /* Goal: Output h-2^255 q, which is between 0 and 2^255-20. */ + + carry0 = h0 >> 26; + h1 += carry0; + h0 -= carry0 << 26; + carry1 = h1 >> 25; + h2 += carry1; + h1 -= carry1 << 25; + carry2 = h2 >> 26; + h3 += carry2; + h2 -= carry2 << 26; + carry3 = h3 >> 25; + h4 += carry3; + h3 -= carry3 << 25; + carry4 = h4 >> 26; + h5 += carry4; + h4 -= carry4 << 26; + carry5 = h5 >> 25; + h6 += carry5; + h5 -= carry5 << 25; + carry6 = h6 >> 26; + h7 += carry6; + h6 -= carry6 << 26; + carry7 = h7 >> 25; + h8 += carry7; + h7 -= carry7 << 25; + carry8 = h8 >> 26; + h9 += carry8; + h8 -= carry8 << 26; + carry9 = h9 >> 25; + h9 -= carry9 << 25; + /* h10 = carry9 */ + + /* + Goal: Output h0+...+2^255 h10-2^255 q, which is between 0 and 2^255-20. + Have h0+...+2^230 h9 between 0 and 2^255-1; + evidently 2^255 h10-2^255 q = 0. + Goal: Output h0+...+2^230 h9. + */ + + s[0] = (byte) (h0 >> 0); + s[1] = (byte) (h0 >> 8); + s[2] = (byte) (h0 >> 16); + s[3] = (byte) ((h0 >> 24) | (h1 << 2)); + s[4] = (byte) (h1 >> 6); + s[5] = (byte) (h1 >> 14); + s[6] = (byte) ((h1 >> 22) | (h2 << 3)); + s[7] = (byte) (h2 >> 5); + s[8] = (byte) (h2 >> 13); + s[9] = (byte) ((h2 >> 21) | (h3 << 5)); + s[10] = (byte) (h3 >> 3); + s[11] = (byte) (h3 >> 11); + s[12] = (byte) ((h3 >> 19) | (h4 << 6)); + s[13] = (byte) (h4 >> 2); + s[14] = (byte) (h4 >> 10); + s[15] = (byte) (h4 >> 18); + s[16] = (byte) (h5 >> 0); + s[17] = (byte) (h5 >> 8); + s[18] = (byte) (h5 >> 16); + s[19] = (byte) ((h5 >> 24) | (h6 << 1)); + s[20] = (byte) (h6 >> 7); + s[21] = (byte) (h6 >> 15); + s[22] = (byte) ((h6 >> 23) | (h7 << 3)); + s[23] = (byte) (h7 >> 5); + s[24] = (byte) (h7 >> 13); + s[25] = (byte) ((h7 >> 21) | (h8 << 4)); + s[26] = (byte) (h8 >> 4); + s[27] = (byte) (h8 >> 12); + s[28] = (byte) ((h8 >> 20) | (h9 << 6)); + s[29] = (byte) (h9 >> 2); + s[30] = (byte) (h9 >> 10); + s[31] = (byte) (h9 >> 18); + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/ge_add.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/ge_add.java new file mode 100755 index 0000000000..81cb95a9bb --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/ge_add.java @@ -0,0 +1,120 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class ge_add { + +//CONVERT #include "ge.h" + +/* +r = p + q +*/ + + public static void ge_add(ge_p1p1 r, ge_p3 p, ge_cached q) { + int[] t0 = new int[10]; +//CONVERT #include "ge_add.h" + +/* qhasm: enter ge_add */ + +/* qhasm: fe X1 */ + +/* qhasm: fe Y1 */ + +/* qhasm: fe Z1 */ + +/* qhasm: fe Z2 */ + +/* qhasm: fe T1 */ + +/* qhasm: fe ZZ */ + +/* qhasm: fe YpX2 */ + +/* qhasm: fe YmX2 */ + +/* qhasm: fe T2d2 */ + +/* qhasm: fe X3 */ + +/* qhasm: fe Y3 */ + +/* qhasm: fe Z3 */ + +/* qhasm: fe T3 */ + +/* qhasm: fe YpX1 */ + +/* qhasm: fe YmX1 */ + +/* qhasm: fe A */ + +/* qhasm: fe B */ + +/* qhasm: fe C */ + +/* qhasm: fe D */ + +/* qhasm: YpX1 = Y1+X1 */ +/* asm 1: fe_add.fe_add(>YpX1=fe#1,YpX1=r.X,YmX1=fe#2,YmX1=r.Y,A=fe#3,A=r.Z,B=fe#2,B=r.Y,C=fe#4,C=r.T,ZZ=fe#1,ZZ=r.X,D=fe#5,D=t0,X3=fe#1,X3=r.X,Y3=fe#2,Y3=r.Y,Z3=fe#3,Z3=r.Z,T3=fe#4,T3=r.T,> 3] >> (i & 7)); + r[i] = (byte) (1 & (a[i >> 3] >>> (i & 7))); + } + + for (i = 0; i < 256; ++i) + if (r[i] != 0) { + for (b = 1; b <= 6 && i + b < 256; ++b) { + if (r[i + b] != 0) { + if (r[i] + (r[i + b] << b) <= 15) { + r[i] += r[i + b] << b; + r[i + b] = 0; + } else if (r[i] - (r[i + b] << b) >= -15) { + r[i] -= r[i + b] << b; + for (k = i + b; k < 256; ++k) { + if (r[k] == 0) { + r[k] = 1; + break; + } + r[k] = 0; + } + } else + break; + } + } + } + + } + + static ge_precomp Bi[]; + + static { + Bi = new ge_precomp[8]; + Bi[0] = new ge_precomp( + new int[]{25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605}, + new int[]{-12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378}, + new int[]{-8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546} + ); + Bi[1] = new ge_precomp( + new int[]{15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024}, + new int[]{16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574}, + new int[]{30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357} + ); + Bi[2] = new ge_precomp( + new int[]{10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380}, + new int[]{4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306}, + new int[]{19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942} + ); + Bi[3] = new ge_precomp( + new int[]{5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766}, + new int[]{-30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701}, + new int[]{28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300} + ); + Bi[4] = new ge_precomp( + new int[]{-22518993, -6692182, 14201702, -8745502, -23510406, 8844726, 18474211, -1361450, -13062696, 13821877}, + new int[]{-6455177, -7839871, 3374702, -4740862, -27098617, -10571707, 31655028, -7212327, 18853322, -14220951}, + new int[]{4566830, -12963868, -28974889, -12240689, -7602672, -2830569, -8514358, -10431137, 2207753, -3209784} + ); + Bi[5] = new ge_precomp( + new int[]{-25154831, -4185821, 29681144, 7868801, -6854661, -9423865, -12437364, -663000, -31111463, -16132436}, + new int[]{25576264, -2703214, 7349804, -11814844, 16472782, 9300885, 3844789, 15725684, 171356, 6466918}, + new int[]{23103977, 13316479, 9739013, -16149481, 817875, -15038942, 8965339, -14088058, -30714912, 16193877} + ); + Bi[6] = new ge_precomp( + new int[]{-33521811, 3180713, -2394130, 14003687, -16903474, -16270840, 17238398, 4729455, -18074513, 9256800}, + new int[]{-25182317, -4174131, 32336398, 5036987, -21236817, 11360617, 22616405, 9761698, -19827198, 630305}, + new int[]{-13720693, 2639453, -24237460, -7406481, 9494427, -5774029, -6554551, -15960994, -2449256, -14291300} + ); + Bi[7] = new ge_precomp( + new int[]{-3151181, -5046075, 9282714, 6866145, -31907062, -863023, -18940575, 15033784, 25105118, -7894876}, + new int[]{-24326370, 15950226, -31801215, -14592823, -11662737, -5090925, 1573892, -2625887, 2198790, -15804619}, + new int[]{-3099351, 10324967, -2241613, 7453183, -5446979, -2735503, -13812022, -16236442, -32461234, -12290683} + ); + } + +/* +r = a * A + b * B +where a = a[0]+256*a[1]+...+256^31 a[31]. +and b = b[0]+256*b[1]+...+256^31 b[31]. +B is the Ed25519 base point (x,4/5) with x positive. +*/ + + public static void ge_double_scalarmult_vartime(ge_p2 r, byte[] a, ge_p3 A, byte[] b) { + byte[] aslide = new byte[256]; + byte[] bslide = new byte[256]; + ge_cached Ai[] = new ge_cached[8]; /* A,3A,5A,7A,9A,11A,13A,15A */ + for (int count = 0; count < 8; count++) + Ai[count] = new ge_cached(); + ge_p1p1 t = new ge_p1p1(); + ge_p3 u = new ge_p3(); + ge_p3 A2 = new ge_p3(); + int i; + + slide(aslide, a); + slide(bslide, b); + + ge_p3_to_cached.ge_p3_to_cached(Ai[0], A); + ge_p3_dbl.ge_p3_dbl(t, A); + ge_p1p1_to_p3.ge_p1p1_to_p3(A2, t); + ge_add.ge_add(t, A2, Ai[0]); + ge_p1p1_to_p3.ge_p1p1_to_p3(u, t); + ge_p3_to_cached.ge_p3_to_cached(Ai[1], u); + ge_add.ge_add(t, A2, Ai[1]); + ge_p1p1_to_p3.ge_p1p1_to_p3(u, t); + ge_p3_to_cached.ge_p3_to_cached(Ai[2], u); + ge_add.ge_add(t, A2, Ai[2]); + ge_p1p1_to_p3.ge_p1p1_to_p3(u, t); + ge_p3_to_cached.ge_p3_to_cached(Ai[3], u); + ge_add.ge_add(t, A2, Ai[3]); + ge_p1p1_to_p3.ge_p1p1_to_p3(u, t); + ge_p3_to_cached.ge_p3_to_cached(Ai[4], u); + ge_add.ge_add(t, A2, Ai[4]); + ge_p1p1_to_p3.ge_p1p1_to_p3(u, t); + ge_p3_to_cached.ge_p3_to_cached(Ai[5], u); + ge_add.ge_add(t, A2, Ai[5]); + ge_p1p1_to_p3.ge_p1p1_to_p3(u, t); + ge_p3_to_cached.ge_p3_to_cached(Ai[6], u); + ge_add.ge_add(t, A2, Ai[6]); + ge_p1p1_to_p3.ge_p1p1_to_p3(u, t); + ge_p3_to_cached.ge_p3_to_cached(Ai[7], u); + + ge_p2_0.ge_p2_0(r); + + for (i = 255; i >= 0; --i) { + if (aslide[i] != 0 || bslide[i] != 0) break; + } + + for (; i >= 0; --i) { + ge_p2_dbl.ge_p2_dbl(t, r); + + if (aslide[i] > 0) { + ge_p1p1_to_p3.ge_p1p1_to_p3(u, t); + ge_add.ge_add(t, u, Ai[aslide[i] / 2]); + } else if (aslide[i] < 0) { + ge_p1p1_to_p3.ge_p1p1_to_p3(u, t); + ge_sub.ge_sub(t, u, Ai[(-aslide[i]) / 2]); + } + + if (bslide[i] > 0) { + ge_p1p1_to_p3.ge_p1p1_to_p3(u, t); + ge_madd.ge_madd(t, u, Bi[bslide[i] / 2]); + } else if (bslide[i] < 0) { + ge_p1p1_to_p3.ge_p1p1_to_p3(u, t); + ge_msub.ge_msub(t, u, Bi[(-bslide[i]) / 2]); + } + + ge_p1p1_to_p2.ge_p1p1_to_p2(r, t); + } + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/ge_frombytes.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/ge_frombytes.java new file mode 100755 index 0000000000..279071419f --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/ge_frombytes.java @@ -0,0 +1,65 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class ge_frombytes { + +//CONVERT #include "ge.h" + + static int[] d = { +//CONVERT #include "d.h" + -10913610, 13857413, -15372611, 6949391, 114729, -8787816, -6275908, -3247719, -18696448, -12055116 + }; + + static int[] sqrtm1 = { +//CONVERT #include "sqrtm1.h" + -32595792, -7943725, 9377950, 3500415, 12389472, -272473, -25146209, -2005654, 326686, 11406482 + }; + + static int ge_frombytes_negate_vartime(ge_p3 h, byte[] s) { + int[] u = new int[10]; + int[] v = new int[10]; + int[] v3 = new int[10]; + int[] vxx = new int[10]; + int[] check = new int[10]; + + fe_frombytes.fe_frombytes(h.Y, s); + fe_1.fe_1(h.Z); + fe_sq.fe_sq(u, h.Y); + fe_mul.fe_mul(v, u, d); + fe_sub.fe_sub(u, u, h.Z); /* u = y^2-1 */ + fe_add.fe_add(v, v, h.Z); /* v = dy^2+1 */ + + fe_sq.fe_sq(v3, v); + fe_mul.fe_mul(v3, v3, v); /* v3 = v^3 */ + fe_sq.fe_sq(h.X, v3); + fe_mul.fe_mul(h.X, h.X, v); + fe_mul.fe_mul(h.X, h.X, u); /* x = uv^7 */ + + fe_pow22523.fe_pow22523(h.X, h.X); /* x = (uv^7)^((q-5)/8) */ + fe_mul.fe_mul(h.X, h.X, v3); + fe_mul.fe_mul(h.X, h.X, u); /* x = uv^3(uv^7)^((q-5)/8) */ + + fe_sq.fe_sq(vxx, h.X); + fe_mul.fe_mul(vxx, vxx, v); + fe_sub.fe_sub(check, vxx, u); /* vx^2-u */ + if (fe_isnonzero.fe_isnonzero(check) != 0) { + fe_add.fe_add(check, vxx, u); /* vx^2+u */ + if (fe_isnonzero.fe_isnonzero(check) != 0) return -1; + fe_mul.fe_mul(h.X, h.X, sqrtm1); + } + + if (fe_isnegative.fe_isnegative(h.X) == ((s[31] >>> 7) & 0x01)) { + fe_neg.fe_neg(h.X, h.X); + } + + fe_mul.fe_mul(h.T, h.X, h.Y); + return 0; + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/ge_madd.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/ge_madd.java new file mode 100755 index 0000000000..9d2fce9174 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/ge_madd.java @@ -0,0 +1,111 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class ge_madd { + +//CONVERT #include "ge.h" + +/* +r = p + q +*/ + + public static void ge_madd(ge_p1p1 r, ge_p3 p, ge_precomp q) { + int[] t0 = new int[10]; +//CONVERT #include "ge_madd.h" + +/* qhasm: enter ge_madd */ + +/* qhasm: fe X1 */ + +/* qhasm: fe Y1 */ + +/* qhasm: fe Z1 */ + +/* qhasm: fe T1 */ + +/* qhasm: fe ypx2 */ + +/* qhasm: fe ymx2 */ + +/* qhasm: fe xy2d2 */ + +/* qhasm: fe X3 */ + +/* qhasm: fe Y3 */ + +/* qhasm: fe Z3 */ + +/* qhasm: fe T3 */ + +/* qhasm: fe YpX1 */ + +/* qhasm: fe YmX1 */ + +/* qhasm: fe A */ + +/* qhasm: fe B */ + +/* qhasm: fe C */ + +/* qhasm: fe D */ + +/* qhasm: YpX1 = Y1+X1 */ +/* asm 1: fe_add.fe_add(>YpX1=fe#1,YpX1=r.X,YmX1=fe#2,YmX1=r.Y,A=fe#3,A=r.Z,B=fe#2,B=r.Y,C=fe#4,C=r.T,D=fe#5,D=t0,X3=fe#1,X3=r.X,Y3=fe#2,Y3=r.Y,Z3=fe#3,Z3=r.Z,T3=fe#4,T3=r.T,YpX1=fe#1,YpX1=r.X,YmX1=fe#2,YmX1=r.Y,A=fe#3,A=r.Z,B=fe#2,B=r.Y,C=fe#4,C=r.T,D=fe#5,D=t0,X3=fe#1,X3=r.X,Y3=fe#2,Y3=r.Y,Z3=fe#3,Z3=r.Z,T3=fe#4,T3=r.T,XX=fe#1,XX=r.X,YY=fe#3,YY=r.Z,B=fe#4,B=r.T,A=fe#2,A=r.Y,AA=fe#5,AA=t0,Y3=fe#2,Y3=r.Y,Z3=fe#3,Z3=r.Z,X3=fe#1,X3=r.X,T3=fe#4,T3=r.T,>>= 31; /* 1: yes; 0: no */ + return y; + } + + static int negative(byte b) { + long x = b; /* 18446744073709551361..18446744073709551615: yes; 0..255: no */ + x >>>= 63; /* 1: yes; 0: no */ + return (int) x; + } + + static void cmov(ge_precomp t, ge_precomp u, int b) { + fe_cmov.fe_cmov(t.yplusx, u.yplusx, b); + fe_cmov.fe_cmov(t.yminusx, u.yminusx, b); + fe_cmov.fe_cmov(t.xy2d, u.xy2d, b); + } + + static void select(ge_precomp t, int pos, byte b) { + ge_precomp base[][] = (pos <= 7 ? ge_precomp_base_0_7.base : + (pos <= 15 ? ge_precomp_base_8_15.base : + (pos <= 23 ? ge_precomp_base_16_23.base : ge_precomp_base_24_31.base))); + + ge_precomp minust = new ge_precomp(); + int bnegative = negative(b); + int babs = b - (((-bnegative) & b) << 1); + + ge_precomp_0.ge_precomp_0(t); + cmov(t, base[pos][0], equal((byte) babs, (byte) 1)); + cmov(t, base[pos][1], equal((byte) babs, (byte) 2)); + cmov(t, base[pos][2], equal((byte) babs, (byte) 3)); + cmov(t, base[pos][3], equal((byte) babs, (byte) 4)); + cmov(t, base[pos][4], equal((byte) babs, (byte) 5)); + cmov(t, base[pos][5], equal((byte) babs, (byte) 6)); + cmov(t, base[pos][6], equal((byte) babs, (byte) 7)); + cmov(t, base[pos][7], equal((byte) babs, (byte) 8)); + fe_copy.fe_copy(minust.yplusx, t.yminusx); + fe_copy.fe_copy(minust.yminusx, t.yplusx); + fe_neg.fe_neg(minust.xy2d, t.xy2d); + cmov(t, minust, bnegative); + } + +/* +h = a * B +where a = a[0]+256*a[1]+...+256^31 a[31] +B is the Ed25519 base point (x,4/5) with x positive. + +Preconditions: + a[31] <= 127 +*/ + + public static void ge_scalarmult_base(ge_p3 h, byte[] a) { + byte[] e = new byte[64]; + byte carry; + ge_p1p1 r = new ge_p1p1(); + ge_p2 s = new ge_p2(); + ge_precomp t = new ge_precomp(); + int i; + + for (i = 0; i < 32; ++i) { + e[2 * i + 0] = (byte) ((a[i] >>> 0) & 15); + e[2 * i + 1] = (byte) ((a[i] >>> 4) & 15); + } + /* each e[i] is between 0 and 15 */ + /* e[63] is between 0 and 7 */ + + carry = 0; + for (i = 0; i < 63; ++i) { + e[i] += carry; + carry = (byte) (e[i] + 8); + carry >>= 4; + e[i] -= carry << 4; + } + e[63] += carry; + /* each e[i] is between -8 and 8 */ + + ge_p3_0.ge_p3_0(h); + for (i = 1; i < 64; i += 2) { + select(t, i / 2, e[i]); + ge_madd.ge_madd(r, h, t); + ge_p1p1_to_p3.ge_p1p1_to_p3(h, r); + } + + ge_p3_dbl.ge_p3_dbl(r, h); + ge_p1p1_to_p2.ge_p1p1_to_p2(s, r); + ge_p2_dbl.ge_p2_dbl(r, s); + ge_p1p1_to_p2.ge_p1p1_to_p2(s, r); + ge_p2_dbl.ge_p2_dbl(r, s); + ge_p1p1_to_p2.ge_p1p1_to_p2(s, r); + ge_p2_dbl.ge_p2_dbl(r, s); + ge_p1p1_to_p3.ge_p1p1_to_p3(h, r); + + for (i = 0; i < 64; i += 2) { + select(t, i / 2, e[i]); + ge_madd.ge_madd(r, h, t); + ge_p1p1_to_p3.ge_p1p1_to_p3(h, r); + } + } + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/ge_sub.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/ge_sub.java new file mode 100755 index 0000000000..274c898f5b --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/ge_sub.java @@ -0,0 +1,120 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class ge_sub { + +//CONVERT #include "ge.h" + +/* +r = p - q +*/ + + public static void ge_sub(ge_p1p1 r, ge_p3 p, ge_cached q) { + int[] t0 = new int[10]; +//CONVERT #include "ge_sub.h" + +/* qhasm: enter ge_sub */ + +/* qhasm: fe X1 */ + +/* qhasm: fe Y1 */ + +/* qhasm: fe Z1 */ + +/* qhasm: fe Z2 */ + +/* qhasm: fe T1 */ + +/* qhasm: fe ZZ */ + +/* qhasm: fe YpX2 */ + +/* qhasm: fe YmX2 */ + +/* qhasm: fe T2d2 */ + +/* qhasm: fe X3 */ + +/* qhasm: fe Y3 */ + +/* qhasm: fe Z3 */ + +/* qhasm: fe T3 */ + +/* qhasm: fe YpX1 */ + +/* qhasm: fe YmX1 */ + +/* qhasm: fe A */ + +/* qhasm: fe B */ + +/* qhasm: fe C */ + +/* qhasm: fe D */ + +/* qhasm: YpX1 = Y1+X1 */ +/* asm 1: fe_add.fe_add(>YpX1=fe#1,YpX1=r.X,YmX1=fe#2,YmX1=r.Y,A=fe#3,A=r.Z,B=fe#2,B=r.Y,C=fe#4,C=r.T,ZZ=fe#1,ZZ=r.X,D=fe#5,D=t0,X3=fe#1,X3=r.X,Y3=fe#2,Y3=r.Y,Z3=fe#3,Z3=r.Z,T3=fe#4,T3=r.T, +//CONVERT #include "crypto_sign.h" +//CONVERT #include "crypto_hash_sha512.h" +//CONVERT #include "crypto_verify_32.h" +//CONVERT #include "ge.h" +//CONVERT #include "sc.h" + + public static int crypto_sign_open( + Sha512 sha512provider, + byte[] m, long mlen, + byte[] sm, long smlen, + byte[] pk + ) { + byte[] pkcopy = new byte[32]; + byte[] rcopy = new byte[32]; + byte[] scopy = new byte[32]; + byte[] h = new byte[64]; + byte[] rcheck = new byte[32]; + ge_p3 A = new ge_p3(); + ge_p2 R = new ge_p2(); + + if (smlen < 64) return -1; + if ((sm[63] & 224) != 0) return -1; + if (ge_frombytes.ge_frombytes_negate_vartime(A, pk) != 0) return -1; + + byte[] pubkeyhash = new byte[64]; + sha512provider.calculateDigest(pubkeyhash, pk, 32); + + System.arraycopy(pk, 0, pkcopy, 0, 32); + System.arraycopy(sm, 0, rcopy, 0, 32); + System.arraycopy(sm, 32, scopy, 0, 32); + + System.arraycopy(sm, 0, m, 0, (int) smlen); + System.arraycopy(pkcopy, 0, m, 32, 32); + sha512provider.calculateDigest(h, m, smlen); + sc_reduce.sc_reduce(h); + + ge_double_scalarmult.ge_double_scalarmult_vartime(R, h, A, scopy); + ge_tobytes.ge_tobytes(rcheck, R); + if (crypto_verify_32.crypto_verify_32(rcheck, rcopy) == 0) { + System.arraycopy(m, 64, m, 0, (int) (smlen - 64)); + //memset(m + smlen - 64,0,64); + return 0; + } + +//badsig: + //memset(m,0,smlen); + return -1; + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/sc_muladd.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/sc_muladd.java new file mode 100755 index 0000000000..d94fc2c843 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/sc_muladd.java @@ -0,0 +1,516 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class sc_muladd { + +//CONVERT #include "sc.h" +//CONVERT #include "long.h" +//CONVERT #include "crypto_uint32.h" +//CONVERT #include "long.h" + + public static long load_3(byte[] in, int index) { + long result; + result = ((long) in[index + 0]) & 0xFF; + result |= (((long) in[index + 1]) << 8) & 0xFF00; + result |= (((long) in[index + 2]) << 16) & 0xFF0000; + return result; + } + + public static long load_4(byte[] in, int index) { + long result; + result = (((long) in[index + 0]) & 0xFF); + result |= ((((long) in[index + 1]) << 8) & 0xFF00); + result |= ((((long) in[index + 2]) << 16) & 0xFF0000); + result |= ((((long) in[index + 3]) << 24) & 0xFF000000L); + return result; + } + +/* +Input: + a[0]+256*a[1]+...+256^31*a[31] = a + b[0]+256*b[1]+...+256^31*b[31] = b + c[0]+256*c[1]+...+256^31*c[31] = c + +Output: + s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l + where l = 2^252 + 27742317777372353535851937790883648493. +*/ + + public static void sc_muladd(byte[] s, byte[] a, byte[] b, byte[] c) { + long a0 = 2097151 & load_3(a, 0); + long a1 = 2097151 & (load_4(a, 2) >>> 5); + long a2 = 2097151 & (load_3(a, 5) >>> 2); + long a3 = 2097151 & (load_4(a, 7) >>> 7); + long a4 = 2097151 & (load_4(a, 10) >>> 4); + long a5 = 2097151 & (load_3(a, 13) >>> 1); + long a6 = 2097151 & (load_4(a, 15) >>> 6); + long a7 = 2097151 & (load_3(a, 18) >>> 3); + long a8 = 2097151 & load_3(a, 21); + long a9 = 2097151 & (load_4(a, 23) >>> 5); + long a10 = 2097151 & (load_3(a, 26) >>> 2); + long a11 = (load_4(a, 28) >>> 7); + long b0 = 2097151 & load_3(b, 0); + long b1 = 2097151 & (load_4(b, 2) >>> 5); + long b2 = 2097151 & (load_3(b, 5) >>> 2); + long b3 = 2097151 & (load_4(b, 7) >>> 7); + long b4 = 2097151 & (load_4(b, 10) >>> 4); + long b5 = 2097151 & (load_3(b, 13) >>> 1); + long b6 = 2097151 & (load_4(b, 15) >>> 6); + long b7 = 2097151 & (load_3(b, 18) >>> 3); + long b8 = 2097151 & load_3(b, 21); + long b9 = 2097151 & (load_4(b, 23) >>> 5); + long b10 = 2097151 & (load_3(b, 26) >>> 2); + long b11 = (load_4(b, 28) >>> 7); + long c0 = 2097151 & load_3(c, 0); + long c1 = 2097151 & (load_4(c, 2) >>> 5); + long c2 = 2097151 & (load_3(c, 5) >>> 2); + long c3 = 2097151 & (load_4(c, 7) >>> 7); + long c4 = 2097151 & (load_4(c, 10) >>> 4); + long c5 = 2097151 & (load_3(c, 13) >>> 1); + long c6 = 2097151 & (load_4(c, 15) >>> 6); + long c7 = 2097151 & (load_3(c, 18) >>> 3); + long c8 = 2097151 & load_3(c, 21); + long c9 = 2097151 & (load_4(c, 23) >>> 5); + long c10 = 2097151 & (load_3(c, 26) >>> 2); + long c11 = (load_4(c, 28) >>> 7); + long s0; + long s1; + long s2; + long s3; + long s4; + long s5; + long s6; + long s7; + long s8; + long s9; + long s10; + long s11; + long s12; + long s13; + long s14; + long s15; + long s16; + long s17; + long s18; + long s19; + long s20; + long s21; + long s22; + long s23; + long carry0; + long carry1; + long carry2; + long carry3; + long carry4; + long carry5; + long carry6; + long carry7; + long carry8; + long carry9; + long carry10; + long carry11; + long carry12; + long carry13; + long carry14; + long carry15; + long carry16; + long carry17; + long carry18; + long carry19; + long carry20; + long carry21; + long carry22; + + s0 = c0 + a0 * b0; + s1 = c1 + a0 * b1 + a1 * b0; + s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0; + s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0; + s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0; + s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0; + s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0; + s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0; + s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1 + a8 * b0; + s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2 + a8 * b1 + a9 * b0; + s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3 + a8 * b2 + a9 * b1 + a10 * b0; + s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4 + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0; + s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3 + a10 * b2 + a11 * b1; + s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3 + a11 * b2; + s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4 + a11 * b3; + s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4; + s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5; + s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6; + s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7; + s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8; + s20 = a9 * b11 + a10 * b10 + a11 * b9; + s21 = a10 * b11 + a11 * b10; + s22 = a11 * b11; + s23 = 0; + + carry0 = (s0 + (1 << 20)) >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; + s13 += carry12; + s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; + s15 += carry14; + s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; + s17 += carry16; + s16 -= carry16 << 21; + carry18 = (s18 + (1 << 20)) >> 21; + s19 += carry18; + s18 -= carry18 << 21; + carry20 = (s20 + (1 << 20)) >> 21; + s21 += carry20; + s20 -= carry20 << 21; + carry22 = (s22 + (1 << 20)) >> 21; + s23 += carry22; + s22 -= carry22 << 21; + + carry1 = (s1 + (1 << 20)) >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; + s14 += carry13; + s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; + s16 += carry15; + s15 -= carry15 << 21; + carry17 = (s17 + (1 << 20)) >> 21; + s18 += carry17; + s17 -= carry17 << 21; + carry19 = (s19 + (1 << 20)) >> 21; + s20 += carry19; + s19 -= carry19 << 21; + carry21 = (s21 + (1 << 20)) >> 21; + s22 += carry21; + s21 -= carry21 << 21; + + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + s23 = 0; + + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + s22 = 0; + + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + s21 = 0; + + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + s20 = 0; + + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + s19 = 0; + + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + s18 = 0; + + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; + s13 += carry12; + s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; + s15 += carry14; + s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; + s17 += carry16; + s16 -= carry16 << 21; + + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; + s14 += carry13; + s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; + s16 += carry15; + s15 -= carry15 << 21; + + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + s17 = 0; + + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + s16 = 0; + + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + s15 = 0; + + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + s14 = 0; + + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + s13 = 0; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = (s0 + (1 << 20)) >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + + carry1 = (s1 + (1 << 20)) >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry11 = s11 >> 21; + s12 += carry11; + s11 -= carry11 << 21; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 << 21; + + s[0] = (byte) (s0 >> 0); + s[1] = (byte) (s0 >> 8); + s[2] = (byte) ((s0 >> 16) | (s1 << 5)); + s[3] = (byte) (s1 >> 3); + s[4] = (byte) (s1 >> 11); + s[5] = (byte) ((s1 >> 19) | (s2 << 2)); + s[6] = (byte) (s2 >> 6); + s[7] = (byte) ((s2 >> 14) | (s3 << 7)); + s[8] = (byte) (s3 >> 1); + s[9] = (byte) (s3 >> 9); + s[10] = (byte) ((s3 >> 17) | (s4 << 4)); + s[11] = (byte) (s4 >> 4); + s[12] = (byte) (s4 >> 12); + s[13] = (byte) ((s4 >> 20) | (s5 << 1)); + s[14] = (byte) (s5 >> 7); + s[15] = (byte) ((s5 >> 15) | (s6 << 6)); + s[16] = (byte) (s6 >> 2); + s[17] = (byte) (s6 >> 10); + s[18] = (byte) ((s6 >> 18) | (s7 << 3)); + s[19] = (byte) (s7 >> 5); + s[20] = (byte) (s7 >> 13); + s[21] = (byte) (s8 >> 0); + s[22] = (byte) (s8 >> 8); + s[23] = (byte) ((s8 >> 16) | (s9 << 5)); + s[24] = (byte) (s9 >> 3); + s[25] = (byte) (s9 >> 11); + s[26] = (byte) ((s9 >> 19) | (s10 << 2)); + s[27] = (byte) (s10 >> 6); + s[28] = (byte) ((s10 >> 14) | (s11 << 7)); + s[29] = (byte) (s11 >> 1); + s[30] = (byte) (s11 >> 9); + s[31] = (byte) (s11 >> 17); + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/sc_reduce.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/sc_reduce.java new file mode 100755 index 0000000000..0394ef92e9 --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/sc_reduce.java @@ -0,0 +1,377 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class sc_reduce { + +//CONVERT #include "sc.h" +//CONVERT #include "long.h" +//CONVERT #include "crypto_uint32.h" +//CONVERT #include "long.h" + + public static long load_3(byte[] in, int index) { + long result; + result = ((long) in[index + 0]) & 0xFF; + result |= (((long) in[index + 1]) << 8) & 0xFF00; + result |= (((long) in[index + 2]) << 16) & 0xFF0000; + return result; + } + + public static long load_4(byte[] in, int index) { + long result; + result = (((long) in[index + 0]) & 0xFF); + result |= ((((long) in[index + 1]) << 8) & 0xFF00); + result |= ((((long) in[index + 2]) << 16) & 0xFF0000); + result |= ((((long) in[index + 3]) << 24) & 0xFF000000L); + return result; + } + +/* +Input: + s[0]+256*s[1]+...+256^63*s[63] = s + +Output: + s[0]+256*s[1]+...+256^31*s[31] = s mod l + where l = 2^252 + 27742317777372353535851937790883648493. + Overwrites s in place. +*/ + + public static void sc_reduce(byte[] s) { + long s0 = 2097151 & load_3(s, 0); + long s1 = 2097151 & (load_4(s, 2) >>> 5); + long s2 = 2097151 & (load_3(s, 5) >>> 2); + long s3 = 2097151 & (load_4(s, 7) >>> 7); + long s4 = 2097151 & (load_4(s, 10) >>> 4); + long s5 = 2097151 & (load_3(s, 13) >>> 1); + long s6 = 2097151 & (load_4(s, 15) >>> 6); + long s7 = 2097151 & (load_3(s, 18) >>> 3); + long s8 = 2097151 & load_3(s, 21); + long s9 = 2097151 & (load_4(s, 23) >>> 5); + long s10 = 2097151 & (load_3(s, 26) >>> 2); + long s11 = 2097151 & (load_4(s, 28) >>> 7); + long s12 = 2097151 & (load_4(s, 31) >>> 4); + long s13 = 2097151 & (load_3(s, 34) >>> 1); + long s14 = 2097151 & (load_4(s, 36) >>> 6); + long s15 = 2097151 & (load_3(s, 39) >>> 3); + long s16 = 2097151 & load_3(s, 42); + long s17 = 2097151 & (load_4(s, 44) >>> 5); + long s18 = 2097151 & (load_3(s, 47) >>> 2); + long s19 = 2097151 & (load_4(s, 49) >>> 7); + long s20 = 2097151 & (load_4(s, 52) >>> 4); + long s21 = 2097151 & (load_3(s, 55) >>> 1); + long s22 = 2097151 & (load_4(s, 57) >>> 6); + long s23 = (load_4(s, 60) >>> 3); + long carry0; + long carry1; + long carry2; + long carry3; + long carry4; + long carry5; + long carry6; + long carry7; + long carry8; + long carry9; + long carry10; + long carry11; + long carry12; + long carry13; + long carry14; + long carry15; + long carry16; + + s11 += s23 * 666643; + s12 += s23 * 470296; + s13 += s23 * 654183; + s14 -= s23 * 997805; + s15 += s23 * 136657; + s16 -= s23 * 683901; + s23 = 0; + + s10 += s22 * 666643; + s11 += s22 * 470296; + s12 += s22 * 654183; + s13 -= s22 * 997805; + s14 += s22 * 136657; + s15 -= s22 * 683901; + s22 = 0; + + s9 += s21 * 666643; + s10 += s21 * 470296; + s11 += s21 * 654183; + s12 -= s21 * 997805; + s13 += s21 * 136657; + s14 -= s21 * 683901; + s21 = 0; + + s8 += s20 * 666643; + s9 += s20 * 470296; + s10 += s20 * 654183; + s11 -= s20 * 997805; + s12 += s20 * 136657; + s13 -= s20 * 683901; + s20 = 0; + + s7 += s19 * 666643; + s8 += s19 * 470296; + s9 += s19 * 654183; + s10 -= s19 * 997805; + s11 += s19 * 136657; + s12 -= s19 * 683901; + s19 = 0; + + s6 += s18 * 666643; + s7 += s18 * 470296; + s8 += s18 * 654183; + s9 -= s18 * 997805; + s10 += s18 * 136657; + s11 -= s18 * 683901; + s18 = 0; + + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry12 = (s12 + (1 << 20)) >> 21; + s13 += carry12; + s12 -= carry12 << 21; + carry14 = (s14 + (1 << 20)) >> 21; + s15 += carry14; + s14 -= carry14 << 21; + carry16 = (s16 + (1 << 20)) >> 21; + s17 += carry16; + s16 -= carry16 << 21; + + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + carry13 = (s13 + (1 << 20)) >> 21; + s14 += carry13; + s13 -= carry13 << 21; + carry15 = (s15 + (1 << 20)) >> 21; + s16 += carry15; + s15 -= carry15 << 21; + + s5 += s17 * 666643; + s6 += s17 * 470296; + s7 += s17 * 654183; + s8 -= s17 * 997805; + s9 += s17 * 136657; + s10 -= s17 * 683901; + s17 = 0; + + s4 += s16 * 666643; + s5 += s16 * 470296; + s6 += s16 * 654183; + s7 -= s16 * 997805; + s8 += s16 * 136657; + s9 -= s16 * 683901; + s16 = 0; + + s3 += s15 * 666643; + s4 += s15 * 470296; + s5 += s15 * 654183; + s6 -= s15 * 997805; + s7 += s15 * 136657; + s8 -= s15 * 683901; + s15 = 0; + + s2 += s14 * 666643; + s3 += s14 * 470296; + s4 += s14 * 654183; + s5 -= s14 * 997805; + s6 += s14 * 136657; + s7 -= s14 * 683901; + s14 = 0; + + s1 += s13 * 666643; + s2 += s13 * 470296; + s3 += s13 * 654183; + s4 -= s13 * 997805; + s5 += s13 * 136657; + s6 -= s13 * 683901; + s13 = 0; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = (s0 + (1 << 20)) >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry2 = (s2 + (1 << 20)) >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry4 = (s4 + (1 << 20)) >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry6 = (s6 + (1 << 20)) >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry8 = (s8 + (1 << 20)) >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry10 = (s10 + (1 << 20)) >> 21; + s11 += carry10; + s10 -= carry10 << 21; + + carry1 = (s1 + (1 << 20)) >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry3 = (s3 + (1 << 20)) >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry5 = (s5 + (1 << 20)) >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry7 = (s7 + (1 << 20)) >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry9 = (s9 + (1 << 20)) >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry11 = (s11 + (1 << 20)) >> 21; + s12 += carry11; + s11 -= carry11 << 21; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 << 21; + carry11 = s11 >> 21; + s12 += carry11; + s11 -= carry11 << 21; + + s0 += s12 * 666643; + s1 += s12 * 470296; + s2 += s12 * 654183; + s3 -= s12 * 997805; + s4 += s12 * 136657; + s5 -= s12 * 683901; + s12 = 0; + + carry0 = s0 >> 21; + s1 += carry0; + s0 -= carry0 << 21; + carry1 = s1 >> 21; + s2 += carry1; + s1 -= carry1 << 21; + carry2 = s2 >> 21; + s3 += carry2; + s2 -= carry2 << 21; + carry3 = s3 >> 21; + s4 += carry3; + s3 -= carry3 << 21; + carry4 = s4 >> 21; + s5 += carry4; + s4 -= carry4 << 21; + carry5 = s5 >> 21; + s6 += carry5; + s5 -= carry5 << 21; + carry6 = s6 >> 21; + s7 += carry6; + s6 -= carry6 << 21; + carry7 = s7 >> 21; + s8 += carry7; + s7 -= carry7 << 21; + carry8 = s8 >> 21; + s9 += carry8; + s8 -= carry8 << 21; + carry9 = s9 >> 21; + s10 += carry9; + s9 -= carry9 << 21; + carry10 = s10 >> 21; + s11 += carry10; + s10 -= carry10 << 21; + + s[0] = (byte) (s0 >> 0); + s[1] = (byte) (s0 >> 8); + s[2] = (byte) ((s0 >> 16) | (s1 << 5)); + s[3] = (byte) (s1 >> 3); + s[4] = (byte) (s1 >> 11); + s[5] = (byte) ((s1 >> 19) | (s2 << 2)); + s[6] = (byte) (s2 >> 6); + s[7] = (byte) ((s2 >> 14) | (s3 << 7)); + s[8] = (byte) (s3 >> 1); + s[9] = (byte) (s3 >> 9); + s[10] = (byte) ((s3 >> 17) | (s4 << 4)); + s[11] = (byte) (s4 >> 4); + s[12] = (byte) (s4 >> 12); + s[13] = (byte) ((s4 >> 20) | (s5 << 1)); + s[14] = (byte) (s5 >> 7); + s[15] = (byte) ((s5 >> 15) | (s6 << 6)); + s[16] = (byte) (s6 >> 2); + s[17] = (byte) (s6 >> 10); + s[18] = (byte) ((s6 >> 18) | (s7 << 3)); + s[19] = (byte) (s7 >> 5); + s[20] = (byte) (s7 >> 13); + s[21] = (byte) (s8 >> 0); + s[22] = (byte) (s8 >> 8); + s[23] = (byte) ((s8 >> 16) | (s9 << 5)); + s[24] = (byte) (s9 >> 3); + s[25] = (byte) (s9 >> 11); + s[26] = (byte) ((s9 >> 19) | (s10 << 2)); + s[27] = (byte) (s10 >> 6); + s[28] = (byte) ((s10 >> 14) | (s11 << 7)); + s[29] = (byte) (s11 >> 1); + s[30] = (byte) (s11 >> 9); + s[31] = (byte) (s11 >> 17); + } + + +} diff --git a/actor-keygen/src/main/java/im/actor/keygen/curve25519/scalarmult.java b/actor-keygen/src/main/java/im/actor/keygen/curve25519/scalarmult.java new file mode 100755 index 0000000000..df134338be --- /dev/null +++ b/actor-keygen/src/main/java/im/actor/keygen/curve25519/scalarmult.java @@ -0,0 +1,200 @@ +package im.actor.keygen.curve25519; + +// Disabling Bounds checks for speeding up calculations + +/*-[ +#define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 +]-*/ + +public class scalarmult { + +//CONVERT #include "crypto_scalarmult.h" +//CONVERT #include "fe.h" + + public static int crypto_scalarmult(byte[] q, + byte[] n, + byte[] p) { + byte[] e = new byte[32]; + int i; + int[] x1 = new int[10]; + int[] x2 = new int[10]; + int[] z2 = new int[10]; + int[] x3 = new int[10]; + int[] z3 = new int[10]; + int[] tmp0 = new int[10]; + int[] tmp1 = new int[10]; + int pos; + int swap; + int b; + + for (i = 0; i < 32; ++i) e[i] = n[i]; +// e[0] &= 248; +// e[31] &= 127; +// e[31] |= 64; + fe_frombytes.fe_frombytes(x1, p); + fe_1.fe_1(x2); + fe_0.fe_0(z2); + fe_copy.fe_copy(x3, x1); + fe_1.fe_1(z3); + + swap = 0; + for (pos = 254; pos >= 0; --pos) { + b = e[pos / 8] >>> (pos & 7); + b &= 1; + swap ^= b; + fe_cswap.fe_cswap(x2, x3, swap); + fe_cswap.fe_cswap(z2, z3, swap); + swap = b; +//CONVERT #include "montgomery.h" + +/* qhasm: fe X2 */ + +/* qhasm: fe Z2 */ + +/* qhasm: fe X3 */ + +/* qhasm: fe Z3 */ + +/* qhasm: fe X4 */ + +/* qhasm: fe Z4 */ + +/* qhasm: fe X5 */ + +/* qhasm: fe Z5 */ + +/* qhasm: fe A */ + +/* qhasm: fe B */ + +/* qhasm: fe C */ + +/* qhasm: fe D */ + +/* qhasm: fe E */ + +/* qhasm: fe AA */ + +/* qhasm: fe BB */ + +/* qhasm: fe DA */ + +/* qhasm: fe CB */ + +/* qhasm: fe t0 */ + +/* qhasm: fe t1 */ + +/* qhasm: fe t2 */ + +/* qhasm: fe t3 */ + +/* qhasm: fe t4 */ + +/* qhasm: enter ladder */ + +/* qhasm: D = X3-Z3 */ +/* asm 1: fe_sub.fe_sub(>D=fe#5,D=tmp0,B=fe#6,B=tmp1,A=fe#1,A=x2,C=fe#2,C=z2,DA=fe#4,DA=z3,CB=fe#2,CB=z2,BB=fe#5,BB=tmp0,AA=fe#6,AA=tmp1,t0=fe#3,t0=x3,t1=fe#2,t1=z2,X4=fe#1,X4=x2,E=fe#6,E=tmp1,t2=fe#2,t2=z2,t3=fe#4,t3=z3,X5=fe#3,X5=x3,t4=fe#5,t4=tmp0,Z5=fe#4,x1,Z5=z3,x1,Z4=fe#2,Z4=z2, +//CONVERT #include "crypto_sign.h" +//CONVERT #include "crypto_hash_sha512.h" +//CONVERT #include "ge.h" +//CONVERT #include "sc.h" +//CONVERT #include "zeroize.h" + + /* NEW: Compare to pristine crypto_sign() + Uses explicit private key for nonce derivation and as scalar, + instead of deriving both from a master key. + */ + static int crypto_sign_modified( + Sha512 sha512provider, + byte[] sm, + byte[] m, long mlen, + byte[] sk, byte[] pk, + byte[] random + ) { + byte[] nonce = new byte[64]; + byte[] hram = new byte[64]; + ge_p3 R = new ge_p3(); + int count = 0; + + System.arraycopy(m, 0, sm, 64, (int) mlen); + System.arraycopy(sk, 0, sm, 32, 32); + + /* NEW : add prefix to separate hash uses - see .h */ + sm[0] = (byte) 0xFE; + for (count = 1; count < 32; count++) + sm[count] = (byte) 0xFF; + + /* NEW: add suffix of random data */ + System.arraycopy(random, 0, sm, (int) (mlen + 64), 64); + + sha512provider.calculateDigest(nonce, sm, mlen + 128); + System.arraycopy(pk, 0, sm, 32, 32); + + sc_reduce.sc_reduce(nonce); + ge_scalarmult_base.ge_scalarmult_base(R, nonce); + ge_p3_tobytes.ge_p3_tobytes(sm, R); + + sha512provider.calculateDigest(hram, sm, mlen + 64); + sc_reduce.sc_reduce(hram); + byte[] S = new byte[32]; + sc_muladd.sc_muladd(S, hram, sk, nonce); /* NEW: Use privkey directly */ + System.arraycopy(S, 0, sm, 32, 32); + + return 0; + } + + +} diff --git a/actor-sdk/sdk-api/actor.json b/actor-sdk/sdk-api/actor.json index ef48ff2e50..b27bd3cb55 100644 --- a/actor-sdk/sdk-api/actor.json +++ b/actor-sdk/sdk-api/actor.json @@ -6601,6 +6601,32 @@ ] } }, + { + "type": "update", + "content": { + "name": "ChatDropCache", + "header": 2690, + "doc": [ + "Update about cache drop", + { + "type": "reference", + "argument": "peer", + "category": "full", + "description": " Destination peer" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "Peer" + }, + "id": 1, + "name": "peer" + } + ] + } + }, { "type": "update", "content": { @@ -6611,7 +6637,7 @@ { "type": "reference", "argument": "dialogs", - "category": "full", + "category": "compact", "description": " New dialgos list" } ], @@ -7876,12 +7902,116 @@ ] } }, + { + "type": "enum", + "content": { + "name": "GroupPermissions", + "values": [ + { + "name": "SEND_MESSAGE", + "id": 1 + }, + { + "name": "CLEAR", + "id": 2 + }, + { + "name": "LEAVE", + "id": 3 + }, + { + "name": "DELETE", + "id": 4 + }, + { + "name": "JOIN", + "id": 5 + }, + { + "name": "VIEW_INFO", + "id": 6 + } + ] + } + }, + { + "type": "enum", + "content": { + "name": "GroupFullPermissions", + "values": [ + { + "name": "EDIT_INFO", + "id": 1 + }, + { + "name": "VIEW_MEMBERS", + "id": 2 + }, + { + "name": "INVITE_MEMBERS", + "id": 3 + }, + { + "name": "INVITE_VIA_LINK", + "id": 4 + }, + { + "name": "CALL", + "id": 5 + }, + { + "name": "EDIT_ADMIN_SETTINGS", + "id": 6 + }, + { + "name": "VIEW_ADMINS", + "id": 7 + }, + { + "name": "EDIT_ADMINS", + "id": 8 + }, + { + "name": "KICK_INVITED", + "id": 9 + }, + { + "name": "KICK_ANYONE", + "id": 10 + }, + { + "name": "EDIT_FOREIGN", + "id": 11 + }, + { + "name": "DELETE_FOREIGN", + "id": 12 + } + ] + } + }, { "type": "struct", "content": { "name": "Group", "doc": [ "Group information", + "", + "Permissions.", + "Permissions of this structure is about group messages operation, such as", + "ability to send messages, clear chat, leave group and so on. This operations", + "Can be held outside of the Group Info page.", + "", + "Default value is ZERO, Opposide iz ONE. If Default is FALSE then ONE == TRUE.", + "If default is TRUE then ONE == FALSE.", + "Bits:", + "0 - canSendMessage. Default is FALSE.", + "1 - canClear. Default is FALSE.", + "2 - canLeave. Default is FALSE.", + "3 - canDelete. Default is FALSE.", + "4 - canJoin. Default is FALSE.", + "5 - canViewInfo. Default is FALSE.", + "", { "type": "reference", "argument": "id", @@ -7932,9 +8062,15 @@ }, { "type": "reference", - "argument": "canSendMessage", + "argument": "permissions", + "category": "full", + "description": " Permissions of group object" + }, + { + "type": "reference", + "argument": "isDeleted", "category": "full", - "description": " Can user send messages. Default is equals isMember for Group and false for others." + "description": " Is this group deleted" }, { "type": "reference", @@ -8048,10 +8184,18 @@ { "type": { "type": "opt", - "childType": "bool" + "childType": "int64" }, "id": 26, - "name": "canSendMessage" + "name": "permissions" + }, + { + "type": { + "type": "opt", + "childType": "bool" + }, + "id": 27, + "name": "isDeleted" }, { "type": { @@ -8070,7 +8214,8 @@ "childType": "bool" }, "id": 16, - "name": "isAdmin" + "name": "isAdmin", + "deprecated": "true" }, { "type": { @@ -8078,7 +8223,8 @@ "childType": "userId" }, "id": 8, - "name": "creatorUid" + "name": "creatorUid", + "deprecated": "true" }, { "type": { @@ -8089,7 +8235,8 @@ } }, "id": 9, - "name": "members" + "name": "members", + "deprecated": "true" }, { "type": { @@ -8097,7 +8244,8 @@ "childType": "date" }, "id": 10, - "name": "createDate" + "name": "createDate", + "deprecated": "true" }, { "type": { @@ -8105,7 +8253,8 @@ "childType": "string" }, "id": 17, - "name": "theme" + "name": "theme", + "deprecated": "true" }, { "type": { @@ -8113,7 +8262,8 @@ "childType": "string" }, "id": 18, - "name": "about" + "name": "about", + "deprecated": "true" } ] } @@ -8124,6 +8274,26 @@ "name": "GroupFull", "doc": [ "Goup Full information", + "Permissions.", + "Idea of Group Full mermissions is about Group Info pages. This permissions", + "are usefull only when trying to view and update group settings and not related", + "to chat messages itself.", + "Default value is ZERO, Opposide iz ONE. If Default is FALSE then ONE == TRUE.", + "If default is TRUE then ONE == FALSE.", + "Bits:", + "0 - canEditInfo. Default is FALSE.", + "1 - canViewMembers. Default is FALSE.", + "2 - canInviteMembers. Default is FALSE.", + "3 - canInviteViaLink. Default is FALSE.", + "4 - canCall. Default is FALSE.", + "5 - canEditAdminSettings. Default is FALSE.", + "6 - canViewAdmins. Default is FALSE.", + "7 - canEditAdmins. Default is FALSE.", + "8 - canKickInvited. Default is FALSE.", + "9 - canKickAnyone. Default is FALSE.", + "10 - canEditForeign. Default is FALSE.", + "11 - canDeleteForeign. Default is FALSE.", + "", { "type": "reference", "argument": "id", @@ -8140,7 +8310,7 @@ "type": "reference", "argument": "ownerUid", "category": "full", - "description": " Group owner" + "description": " Optional group owner" }, { "type": "reference", @@ -8168,23 +8338,24 @@ }, { "type": "reference", - "argument": "canViewMembers", + "argument": "isSharedHistory", "category": "full", - "description": " Can current user view members of the group. Default is true." + "description": " Is history shared among all users. Default is false." }, { "type": "reference", - "argument": "canInvitePeople", + "argument": "shortName", "category": "full", - "description": " Can current user invite new people. Default is true." + "description": " Group's short name" }, { "type": "reference", - "argument": "isSharedHistory", + "argument": "permissions", "category": "full", - "description": " Is history shared among all users. Default is false." + "description": " Group Permissions" } ], + "expandable": "true", "attributes": [ { "type": { @@ -8204,8 +8375,11 @@ }, { "type": { - "type": "alias", - "childType": "userId" + "type": "opt", + "childType": { + "type": "alias", + "childType": "userId" + } }, "id": 5, "name": "ownerUid" @@ -8261,24 +8435,24 @@ "type": "opt", "childType": "bool" }, - "id": 8, - "name": "canViewMembers" + "id": 10, + "name": "isSharedHistory" }, { "type": { "type": "opt", - "childType": "bool" + "childType": "string" }, - "id": 9, - "name": "canInvitePeople" + "id": 14, + "name": "shortName" }, { "type": { "type": "opt", - "childType": "bool" + "childType": "int64" }, - "id": 10, - "name": "isSharedHistory" + "id": 27, + "name": "permissions" } ] } @@ -8348,7 +8522,7 @@ "doc": [ { "type": "reference", - "argument": "members", + "argument": "users", "category": "full", "description": " Group members" }, @@ -8360,6 +8534,17 @@ } ], "attributes": [ + { + "type": { + "type": "list", + "childType": { + "type": "struct", + "childType": "Member" + } + }, + "id": 3, + "name": "members" + }, { "type": { "type": "list", @@ -8369,7 +8554,7 @@ } }, "id": 1, - "name": "members" + "name": "users" }, { "type": { @@ -8677,6 +8862,46 @@ ] } }, + { + "type": "update", + "content": { + "name": "GroupShortNameChanged", + "header": 2628, + "doc": [ + "Group's short name changed", + { + "type": "reference", + "argument": "groupId", + "category": "full", + "description": " Group Id" + }, + { + "type": "reference", + "argument": "shortName", + "category": "full", + "description": " Group short name" + } + ], + "attributes": [ + { + "type": { + "type": "alias", + "childType": "groupId" + }, + "id": 1, + "name": "groupId" + }, + { + "type": { + "type": "opt", + "childType": "string" + }, + "id": 2, + "name": "shortName" + } + ] + } + }, { "type": "update", "content": { @@ -8746,21 +8971,15 @@ { "type": "update", "content": { - "name": "GroupCanSendMessagesChanged", - "header": 2624, + "name": "GroupDeleted", + "header": 2658, "doc": [ - "Update about can send messages changed", + "Update about group deleted", { "type": "reference", "argument": "groupId", "category": "full", "description": " Group Id" - }, - { - "type": "reference", - "argument": "canSendMessages", - "category": "full", - "description": " Can send messages" } ], "attributes": [ @@ -8771,11 +8990,6 @@ }, "id": 1, "name": "groupId" - }, - { - "type": "bool", - "id": 2, - "name": "canSendMessages" } ] } @@ -8783,10 +8997,10 @@ { "type": "update", "content": { - "name": "GroupCanViewMembersChanged", - "header": 2625, + "name": "GroupPermissionsChanged", + "header": 2663, "doc": [ - "Update about can view members changed", + "Update about group permissions changed", { "type": "reference", "argument": "groupId", @@ -8795,9 +9009,9 @@ }, { "type": "reference", - "argument": "canViewMembers", + "argument": "permissions", "category": "full", - "description": " Can view members" + "description": " New Permissions" } ], "attributes": [ @@ -8810,9 +9024,9 @@ "name": "groupId" }, { - "type": "bool", + "type": "int64", "id": 2, - "name": "canViewMembers" + "name": "permissions" } ] } @@ -8820,10 +9034,10 @@ { "type": "update", "content": { - "name": "GroupCanInviteMembersChanged", - "header": 2626, + "name": "GroupFullPermissionsChanged", + "header": 2664, "doc": [ - "Update about can invite members changed", + "Update about Full Group permissions changed", { "type": "reference", "argument": "groupId", @@ -8832,9 +9046,9 @@ }, { "type": "reference", - "argument": "canInviteMembers", + "argument": "permissions", "category": "full", - "description": " Can invite members" + "description": " New Permissions" } ], "attributes": [ @@ -8847,9 +9061,9 @@ "name": "groupId" }, { - "type": "bool", + "type": "int64", "id": 2, - "name": "canInviteMembers" + "name": "permissions" } ] } @@ -9571,6 +9785,50 @@ ] } }, + { + "type": "rpc", + "content": { + "name": "EditGroupShortName", + "header": 2793, + "response": { + "type": "reference", + "name": "Seq" + }, + "doc": [ + "Edit Group Short Name", + { + "type": "reference", + "argument": "groupPeer", + "category": "full", + "description": "Group's peer" + }, + { + "type": "reference", + "argument": "shortName", + "category": "full", + "description": "New group's short name" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "GroupOutPeer" + }, + "id": 1, + "name": "groupPeer" + }, + { + "type": { + "type": "opt", + "childType": "string" + }, + "id": 2, + "name": "shortName" + } + ] + } + }, { "type": "rpc", "content": { @@ -9806,24 +10064,430 @@ "name": "SeqDate" }, "doc": [ - "Leaving group", + "Leaving group", + { + "type": "reference", + "argument": "groupPeer", + "category": "full", + "description": "Group's peer" + }, + { + "type": "reference", + "argument": "rid", + "category": "full", + "description": "Random Id of operation" + }, + { + "type": "reference", + "argument": "optimizations", + "category": "full", + "description": "Enabled Optimizations" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "GroupOutPeer" + }, + "id": 1, + "name": "groupPeer" + }, + { + "type": { + "type": "alias", + "childType": "randomId" + }, + "id": 2, + "name": "rid" + }, + { + "type": { + "type": "list", + "childType": { + "type": "enum", + "childType": "UpdateOptimization" + } + }, + "id": 3, + "name": "optimizations" + } + ] + } + }, + { + "type": "rpc", + "content": { + "name": "LeaveAndDelete", + "header": 2721, + "response": { + "type": "reference", + "name": "Seq" + }, + "doc": [ + "Leave group and Delete Chat", + { + "type": "reference", + "argument": "groupPeer", + "category": "full", + "description": "Group peer" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "GroupOutPeer" + }, + "id": 1, + "name": "groupPeer" + } + ] + } + }, + { + "type": "rpc", + "content": { + "name": "KickUser", + "header": 71, + "response": { + "type": "reference", + "name": "SeqDate" + }, + "doc": [ + "Kicking user from group", + { + "type": "reference", + "argument": "groupPeer", + "category": "full", + "description": "Group's peer" + }, + { + "type": "reference", + "argument": "user", + "category": "full", + "description": "users for removing" + }, + { + "type": "reference", + "argument": "rid", + "category": "full", + "description": "Random Id of operation" + }, + { + "type": "reference", + "argument": "optimizations", + "category": "full", + "description": "Enabled Optimizations" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "GroupOutPeer" + }, + "id": 1, + "name": "groupPeer" + }, + { + "type": { + "type": "alias", + "childType": "randomId" + }, + "id": 4, + "name": "rid" + }, + { + "type": { + "type": "struct", + "childType": "UserOutPeer" + }, + "id": 3, + "name": "user" + }, + { + "type": { + "type": "list", + "childType": { + "type": "enum", + "childType": "UpdateOptimization" + } + }, + "id": 5, + "name": "optimizations" + } + ] + } + }, + { + "type": "rpc", + "content": { + "name": "JoinGroupByPeer", + "header": 2722, + "response": { + "type": "reference", + "name": "Seq" + }, + "doc": [ + "Join group by peer", + { + "type": "reference", + "argument": "groupPeer", + "category": "full", + "description": "Group's peer" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "GroupOutPeer" + }, + "id": 1, + "name": "groupPeer" + } + ] + } + }, + { + "type": "comment", + "content": "Administration" + }, + { + "type": "rpc", + "content": { + "name": "MakeUserAdmin", + "header": 2784, + "response": { + "type": "reference", + "name": "SeqDate" + }, + "doc": [ + "Make user admin", + { + "type": "reference", + "argument": "groupPeer", + "category": "full", + "description": "Group's peer" + }, + { + "type": "reference", + "argument": "userPeer", + "category": "full", + "description": "User's peer" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "GroupOutPeer" + }, + "id": 1, + "name": "groupPeer" + }, + { + "type": { + "type": "struct", + "childType": "UserOutPeer" + }, + "id": 2, + "name": "userPeer" + } + ] + } + }, + { + "type": "rpc", + "content": { + "name": "DismissUserAdmin", + "header": 2791, + "response": { + "type": "reference", + "name": "Seq" + }, + "doc": [ + "Dismissing user admin", + { + "type": "reference", + "argument": "groupPeer", + "category": "full", + "description": "Group's peer" + }, + { + "type": "reference", + "argument": "userPeer", + "category": "full", + "description": "User's peer" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "GroupOutPeer" + }, + "id": 1, + "name": "groupPeer" + }, + { + "type": { + "type": "struct", + "childType": "UserOutPeer" + }, + "id": 2, + "name": "userPeer" + } + ] + } + }, + { + "type": "rpc", + "content": { + "name": "TransferOwnership", + "header": 2789, + "response": { + "type": "reference", + "name": "SeqDate" + }, + "doc": [ + "Transfer ownership of group", + { + "type": "reference", + "argument": "groupPeer", + "category": "full", + "description": "Group's peer" + }, + { + "type": "reference", + "argument": "newOwner", + "category": "full", + "description": "New group's owner" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "GroupOutPeer" + }, + "id": 1, + "name": "groupPeer" + }, + { + "type": { + "type": "struct", + "childType": "UserOutPeer" + }, + "id": 2, + "name": "newOwner" + } + ] + } + }, + { + "type": "struct", + "content": { + "name": "AdminSettings", + "doc": [ + "Admin Settings", + { + "type": "reference", + "argument": "showAdminsToMembers", + "category": "full", + "description": " Show admins in member list" + }, + { + "type": "reference", + "argument": "canMembersInvite", + "category": "full", + "description": " Can members of a group invite people" + }, + { + "type": "reference", + "argument": "canMembersEditGroupInfo", + "category": "full", + "description": " Can members edit group info" + }, + { + "type": "reference", + "argument": "canAdminsEditGroupInfo", + "category": "full", + "description": " Can admins edit group info" + }, + { + "type": "reference", + "argument": "showJoinLeaveMessages", + "category": "full", + "description": " Should join and leave messages be visible to members" + } + ], + "expandable": "true", + "attributes": [ + { + "type": "bool", + "id": 1, + "name": "showAdminsToMembers" + }, + { + "type": "bool", + "id": 2, + "name": "canMembersInvite" + }, + { + "type": "bool", + "id": 3, + "name": "canMembersEditGroupInfo" + }, + { + "type": "bool", + "id": 4, + "name": "canAdminsEditGroupInfo" + }, + { + "type": "bool", + "id": 5, + "name": "showJoinLeaveMessages" + } + ] + } + }, + { + "type": "rpc", + "content": { + "name": "LoadAdminSettings", + "header": 2790, + "response": { + "type": "anonymous", + "header": 2794, + "doc": [ + "Loaded settings", + { + "type": "reference", + "argument": "settings", + "category": "full", + "description": " Current group admin settings" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "AdminSettings" + }, + "id": 1, + "name": "settings" + } + ] + }, + "doc": [ + "Loading administration settings", { "type": "reference", "argument": "groupPeer", "category": "full", "description": "Group's peer" - }, - { - "type": "reference", - "argument": "rid", - "category": "full", - "description": "Random Id of operation" - }, - { - "type": "reference", - "argument": "optimizations", - "category": "full", - "description": "Enabled Optimizations" } ], "attributes": [ @@ -9834,25 +10498,6 @@ }, "id": 1, "name": "groupPeer" - }, - { - "type": { - "type": "alias", - "childType": "randomId" - }, - "id": 2, - "name": "rid" - }, - { - "type": { - "type": "list", - "childType": { - "type": "enum", - "childType": "UpdateOptimization" - } - }, - "id": 3, - "name": "optimizations" } ] } @@ -9860,37 +10505,25 @@ { "type": "rpc", "content": { - "name": "KickUser", - "header": 71, + "name": "SaveAdminSettings", + "header": 2792, "response": { "type": "reference", - "name": "SeqDate" + "name": "Void" }, "doc": [ - "Kicking user from group", + "Save administartion settings", { "type": "reference", "argument": "groupPeer", "category": "full", - "description": "Group's peer" - }, - { - "type": "reference", - "argument": "user", - "category": "full", - "description": "users for removing" - }, - { - "type": "reference", - "argument": "rid", - "category": "full", - "description": "Random Id of operation" + "description": "Group's Peer" }, { "type": "reference", - "argument": "optimizations", + "argument": "settings", "category": "full", - "description": "Enabled Optimizations" + "description": "Group's settings" } ], "attributes": [ @@ -9902,32 +10535,13 @@ "id": 1, "name": "groupPeer" }, - { - "type": { - "type": "alias", - "childType": "randomId" - }, - "id": 4, - "name": "rid" - }, { "type": { "type": "struct", - "childType": "UserOutPeer" - }, - "id": 3, - "name": "user" - }, - { - "type": { - "type": "list", - "childType": { - "type": "enum", - "childType": "UpdateOptimization" - } + "childType": "AdminSettings" }, - "id": 5, - "name": "optimizations" + "id": 2, + "name": "settings" } ] } @@ -9935,25 +10549,19 @@ { "type": "rpc", "content": { - "name": "MakeUserAdmin", - "header": 2784, + "name": "DeleteGroup", + "header": 2795, "response": { "type": "reference", - "name": "SeqDate" + "name": "Seq" }, "doc": [ - "Make user admin", + "Delete Group", { "type": "reference", "argument": "groupPeer", "category": "full", "description": "Group's peer" - }, - { - "type": "reference", - "argument": "userPeer", - "category": "full", - "description": "User's peer" } ], "attributes": [ @@ -9964,14 +10572,6 @@ }, "id": 1, "name": "groupPeer" - }, - { - "type": { - "type": "struct", - "childType": "UserOutPeer" - }, - "id": 2, - "name": "userPeer" } ] } @@ -9979,25 +10579,19 @@ { "type": "rpc", "content": { - "name": "TransferOwnership", - "header": 2789, + "name": "ShareHistory", + "header": 2796, "response": { "type": "reference", - "name": "SeqDate" + "name": "Seq" }, "doc": [ - "Transfer ownership of group", + "Share History", { "type": "reference", "argument": "groupPeer", "category": "full", "description": "Group's peer" - }, - { - "type": "reference", - "argument": "newOwner", - "category": "full", - "description": "New group's owner" } ], "attributes": [ @@ -10008,14 +10602,6 @@ }, "id": 1, "name": "groupPeer" - }, - { - "type": { - "type": "alias", - "childType": "userId" - }, - "id": 2, - "name": "newOwner" } ] } @@ -11926,39 +12512,9 @@ }, { "type": "reference", - "argument": "title", - "category": "full", - "description": " Peer title" - }, - { - "type": "reference", - "argument": "description", + "argument": "optMatchString", "category": "full", "description": " Description" - }, - { - "type": "reference", - "argument": "membersCount", - "category": "full", - "description": " Members count" - }, - { - "type": "reference", - "argument": "dateCreated", - "category": "full", - "description": " Group Creation Date" - }, - { - "type": "reference", - "argument": "creator", - "category": "full", - "description": " Group Creator uid" - }, - { - "type": "reference", - "argument": "isPublic", - "category": "full", - "description": " Is group public" } ], "attributes": [ @@ -11970,61 +12526,13 @@ "id": 1, "name": "peer" }, - { - "type": "string", - "id": 2, - "name": "title" - }, { "type": { "type": "opt", "childType": "string" }, "id": 3, - "name": "description" - }, - { - "type": { - "type": "opt", - "childType": "int32" - }, - "id": 4, - "name": "membersCount" - }, - { - "type": { - "type": "opt", - "childType": { - "type": "alias", - "childType": "date" - } - }, - "id": 5, - "name": "dateCreated" - }, - { - "type": { - "type": "opt", - "childType": "int32" - }, - "id": 6, - "name": "creator" - }, - { - "type": { - "type": "opt", - "childType": "bool" - }, - "id": 7, - "name": "isPublic" - }, - { - "type": { - "type": "opt", - "childType": "bool" - }, - "id": 8, - "name": "isJoined" + "name": "optMatchString" } ] } @@ -19066,7 +19574,7 @@ { "type": "reference", "argument": "keyGroupId", - "category": "hidden", + "category": "full", "description": " Key Group Id" } ], @@ -19090,6 +19598,45 @@ ] } }, + { + "type": "struct", + "content": { + "name": "KeyGroupHolder", + "doc": [ + "Key Group Holder", + { + "type": "reference", + "argument": "uid", + "category": "full", + "description": " User's id" + }, + { + "type": "reference", + "argument": "keyGroup", + "category": "full", + "description": " Key Group" + } + ], + "attributes": [ + { + "type": { + "type": "alias", + "childType": "userId" + }, + "id": 1, + "name": "uid" + }, + { + "type": { + "type": "struct", + "childType": "EncryptionKeyGroup" + }, + "id": 2, + "name": "keyGroup" + } + ] + } + }, { "type": "rpc", "content": { @@ -19099,18 +19646,6 @@ "type": "anonymous", "header": 2664, "doc": [ - { - "type": "reference", - "argument": "seq", - "category": "full", - "description": " seq" - }, - { - "type": "reference", - "argument": "state", - "category": "full", - "description": " state" - }, { "type": "reference", "argument": "date", @@ -19121,7 +19656,7 @@ "type": "reference", "argument": "obsoleteKeyGroups", "category": "full", - "description": " obsolete key groups" + "description": " obsolete key group ids" }, { "type": "reference", @@ -19131,25 +19666,6 @@ } ], "attributes": [ - { - "type": { - "type": "opt", - "childType": "int32" - }, - "id": 1, - "name": "seq" - }, - { - "type": { - "type": "opt", - "childType": { - "type": "alias", - "childType": "seq_state" - } - }, - "id": 2, - "name": "state" - }, { "type": { "type": "opt", @@ -19158,7 +19674,7 @@ "childType": "date" } }, - "id": 3, + "id": 1, "name": "date" }, { @@ -19169,7 +19685,7 @@ "childType": "KeyGroupId" } }, - "id": 4, + "id": 2, "name": "obsoleteKeyGroups" }, { @@ -19177,10 +19693,10 @@ "type": "list", "childType": { "type": "struct", - "childType": "KeyGroupId" + "childType": "KeyGroupHolder" } }, - "id": 5, + "id": 3, "name": "missedKeyGroups" } ] diff --git a/actor-sdk/sdk-api/api-language/solutions/im.actor.api/models/im/actor/api/scheme.mps b/actor-sdk/sdk-api/api-language/solutions/im.actor.api/models/im/actor/api/scheme.mps index be0622f54c..dc46e3760b 100644 --- a/actor-sdk/sdk-api/api-language/solutions/im.actor.api/models/im/actor/api/scheme.mps +++ b/actor-sdk/sdk-api/api-language/solutions/im.actor.api/models/im/actor/api/scheme.mps @@ -5878,6 +5878,27 @@ + + + + + + + + + + + + + + + + + + + + + @@ -5896,7 +5917,7 @@ - + @@ -6921,12 +6942,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -6969,8 +7116,13 @@ - - + + + + + + + @@ -7063,9 +7215,16 @@ - + - + + + + + + + + @@ -7080,7 +7239,7 @@ - + @@ -7088,7 +7247,7 @@ - + @@ -7096,7 +7255,7 @@ - + @@ -7106,7 +7265,7 @@ - + @@ -7114,7 +7273,7 @@ - + @@ -7122,7 +7281,7 @@ - + @@ -7130,6 +7289,7 @@ + @@ -7147,8 +7307,10 @@ - - + + + + @@ -7190,20 +7352,6 @@ - - - - - - - - - - - - - - @@ -7211,9 +7359,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -7225,7 +7447,7 @@ - + @@ -7245,24 +7467,24 @@ - + - - + + - + - + + - - + - - + - + + @@ -7332,9 +7554,18 @@ + + + + + + + + + - + @@ -7354,7 +7585,7 @@ - + @@ -7586,6 +7817,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -7640,97 +7904,87 @@ - - - + + + - + - - - - - - - + + - - + + - - + - - - - - + - - - + + + - + - + - - + + - - + + - - + + - + - + - - + + - + - - - + + + - + - + - - + + - - + + - - + + - + - + - + + - - + @@ -8331,6 +8585,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -8577,6 +8867,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -8639,6 +8953,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -8685,6 +9026,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -8697,8 +9084,8 @@ - - + + @@ -8721,6 +9108,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -10306,55 +10875,13 @@ - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -10363,35 +10890,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -16159,9 +16661,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -16207,24 +16740,8 @@ - - - - - - - - - - - - - - - - - + @@ -16233,7 +16750,7 @@ - + @@ -16242,27 +16759,17 @@ - + - - + + - - - - - - - - - - @@ -16270,7 +16777,7 @@ - + diff --git a/actor-sdk/sdk-core-android/android-app/build.gradle b/actor-sdk/sdk-core-android/android-app/build.gradle index d754bce2e0..64f4e5c3ce 100644 --- a/actor-sdk/sdk-core-android/android-app/build.gradle +++ b/actor-sdk/sdk-core-android/android-app/build.gradle @@ -1,7 +1,8 @@ buildscript { dependencies { - classpath 'com.android.tools.build:gradle:2.1.2' + classpath 'com.android.tools.build:gradle:2.1.3' classpath 'me.tatarka:gradle-retrolambda:3.2.5' + classpath "com.google.gms:google-services:3.0.0" } } @@ -18,6 +19,7 @@ android { targetSdkVersion 24 versionCode 1 versionName "1.0" + multiDexEnabled true } buildTypes { release { @@ -43,4 +45,5 @@ dependencies { compile project(':actor-sdk:sdk-core-android:android-google-maps') compile 'com.roughike:bottom-bar:1.3.9' + compile 'com.android.support:multidex:1.0.1' } diff --git a/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/Application.java b/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/Application.java index 2ff6af3464..d737d254b9 100644 --- a/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/Application.java +++ b/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/Application.java @@ -1,6 +1,11 @@ package im.actor; +import android.graphics.Color; +import android.graphics.PorterDuff; import android.os.Bundle; +import android.support.multidex.MultiDex; +import android.support.v4.app.Fragment; +import android.text.Spannable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -9,17 +14,29 @@ import org.jetbrains.annotations.Nullable; -import java.util.List; +import java.util.ArrayList; +import im.actor.core.entity.Message; import im.actor.core.entity.Peer; +import im.actor.core.entity.content.JsonContent; +import im.actor.core.entity.content.PhotoContent; import im.actor.develop.R; +import im.actor.fragments.AttachFragmentEx; +import im.actor.fragments.RootFragmentEx; +import im.actor.runtime.json.JSONException; +import im.actor.runtime.json.JSONObject; import im.actor.sdk.ActorSDK; import im.actor.sdk.ActorSDKApplication; import im.actor.sdk.ActorStyle; import im.actor.sdk.BaseActorSDKDelegate; -import im.actor.sdk.controllers.conversation.attach.ShareMenuField; import im.actor.sdk.controllers.conversation.attach.AbsAttachFragment; -import im.actor.sdk.controllers.conversation.attach.AttachFragment; +import im.actor.sdk.controllers.conversation.messages.BubbleLayouter; +import im.actor.sdk.controllers.conversation.messages.DefaultLayouter; +import im.actor.sdk.controllers.conversation.messages.JsonXmlBubbleLayouter; +import im.actor.sdk.controllers.conversation.messages.XmlBubbleLayouter; +import im.actor.sdk.controllers.conversation.messages.content.PhotoHolder; +import im.actor.sdk.controllers.conversation.messages.content.TextHolder; +import im.actor.sdk.controllers.conversation.messages.content.preprocessor.PreprocessedData; import im.actor.sdk.controllers.settings.ActorSettingsCategories; import im.actor.sdk.controllers.settings.ActorSettingsCategory; import im.actor.sdk.controllers.settings.ActorSettingsField; @@ -29,10 +46,17 @@ public class Application extends ActorSDKApplication { + @Override + public void onCreate() { + MultiDex.install(this); + super.onCreate(); + } + @Override public void onConfigureActorSDK() { ActorSDK.sharedActor().setDelegate(new ActorSDKDelegate()); ActorSDK.sharedActor().setPushId(209133700967L); + ActorSDK.sharedActor().setOnClientPrivacyEnabled(true); ActorStyle style = ActorSDK.sharedActor().style; style.setDialogsActiveTextColor(0xff5882ac); @@ -47,6 +71,10 @@ public void onConfigureActorSDK() { ActorSDK.sharedActor().setVideoCallsEnabled(true); + ActorSDK.sharedActor().setAutoJoinGroups(new String[]{ + "actor_news" + }); + // ActorSDK.sharedActor().setTwitter(""); // ActorSDK.sharedActor().setHomePage("http://www.foo.com"); @@ -73,26 +101,53 @@ public void onConfigureActorSDK() { private class ActorSDKDelegate extends BaseActorSDKDelegate { - @Nullable @Override - public AbsAttachFragment fragmentForAttachMenu(Peer peer) { - return new AttachFragment(peer) { - + public void configureChatViewHolders(ArrayList layouters) { +// layouters.add(0, new BubbleTextHolderLayouter()); + layouters.add(0, new DefaultLayouter(DefaultLayouter.TEXT_HOLDER, (adapter2, root2, peer2) -> new TextHolder(adapter2, root2, peer2){ @Override - protected List onCreateFields() { - List res = super.onCreateFields(); - res.add(new ShareMenuField(R.id.share_test, R.drawable.ic_edit_white_24dp, ActorSDK.sharedActor().style.getAccentColor(), "lol")); - return res; + public void bindRawText(CharSequence rawText, long readDate, long receiveDate, Spannable reactions, Message message, boolean isItalic) { + super.bindRawText(rawText, readDate, receiveDate, reactions, message, isItalic); + text.append("\n\n" + message.getSortDate()); } + })); + + layouters.add(0, new DefaultLayouter(DefaultLayouter.TEXT_HOLDER, CensoredTextHolderEx::new)); + layouters.add(0, new XmlBubbleLayouter(content -> content instanceof PhotoContent, R.layout.adapter_dialog_photo, (adapter1, root1, peer1) -> new PhotoHolder(adapter1, root1, peer1) { + @Override + protected void onConfigureViewHolder() { + previewView.setColorFilter(ActorStyle.adjustColorAlpha(Color.CYAN, 20), PorterDuff.Mode.ADD); + } + })); + layouters.add(0, new JsonXmlBubbleLayouter(null, R.layout.adapter_dialog_text, (adapter, root, peer) -> new TextHolder(adapter, root, peer) { @Override - protected void onItemClicked(int id) { - super.onItemClicked(id); - if (id == R.id.share_test) { - Toast.makeText(getContext(), "Hey", Toast.LENGTH_LONG).show(); + protected void bindData(Message message, long readDate, long receiveDate, boolean isUpdated, PreprocessedData preprocessedData) { + String jsonString = "can't read json"; + try { + JSONObject jsonObject = new JSONObject(((JsonContent) message.getContent()).getRawJson()); + String dataType = jsonObject.getString("dataType"); + JSONObject data = jsonObject.getJSONObject("data"); + jsonString = dataType + "\n\n"; + jsonString += data.toString(3); + } catch (JSONException e) { + e.printStackTrace(); } + bindRawText(jsonString, readDate, receiveDate, reactions, message, false); } - }; + })); + } + + @Nullable + @Override + public Fragment fragmentForRoot() { + return new RootFragmentEx(); + } + + @Nullable + @Override + public AbsAttachFragment fragmentForAttachMenu(Peer peer) { + return new AttachFragmentEx(peer); } // @Override diff --git a/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/CensoredTextHolderEx.java b/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/CensoredTextHolderEx.java new file mode 100644 index 0000000000..bec7683773 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/CensoredTextHolderEx.java @@ -0,0 +1,76 @@ +package im.actor; + +import android.graphics.Color; +import android.graphics.Paint; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.style.ClickableSpan; +import android.text.style.URLSpan; +import android.view.View; + +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import im.actor.core.entity.GroupMember; +import im.actor.core.entity.Message; +import im.actor.core.entity.Peer; +import im.actor.core.viewmodel.UserVM; +import im.actor.sdk.ActorSDK; +import im.actor.sdk.controllers.conversation.messages.MessagesAdapter; +import im.actor.sdk.controllers.conversation.messages.content.TextHolder; +import im.actor.sdk.controllers.conversation.view.MentionSpan; +import im.actor.sdk.view.BaseUrlSpan; + +import static im.actor.sdk.util.ActorSDKMessenger.users; + +public class CensoredTextHolderEx extends TextHolder { + public CensoredTextHolderEx(MessagesAdapter adapter, View itemView, Peer peer) { + super(adapter, itemView, peer); + } + + private static ArrayList badWords; + + static { + badWords = new ArrayList<>(); + badWords.add("fuck"); + badWords.add("poke"); + badWords.add("poké"); + } + + @Override + public void bindRawText(CharSequence rawText, long readDate, long receiveDate, Spannable reactions, Message message, boolean isItalic) { + Spannable res = new SpannableString(rawText); + for (String s : badWords) { +// rawText = rawText.toString().replaceAll("/*(?i)" + s + "/*", new String(new char[s.length()]).replace('\0', '*')); + Pattern p = Pattern.compile("/*(?i)" + s + "/*"); + Matcher m = p.matcher(rawText.toString()); + while (m.find()) { + CensorSpan span = new CensorSpan(); + res.setSpan(span, m.start(), m.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + + super.bindRawText(res, readDate, receiveDate, reactions, message, isItalic); + } + + private class CensorSpan extends ClickableSpan { + + @Override + public void updateDrawState(TextPaint ds) { + super.updateDrawState(ds); + ds.setColor(Color.BLACK); + ds.bgColor = Color.BLACK; + ds.setUnderlineText(false); + } + + @Override + public void onClick(View view) { + + } + } +} diff --git a/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/fragments/AttachFragmentEx.java b/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/fragments/AttachFragmentEx.java new file mode 100644 index 0000000000..2a527773f8 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/fragments/AttachFragmentEx.java @@ -0,0 +1,36 @@ +package im.actor.fragments; + +import android.widget.Toast; + +import java.util.List; + +import im.actor.core.entity.Peer; +import im.actor.develop.R; +import im.actor.sdk.ActorSDK; +import im.actor.sdk.controllers.conversation.attach.AttachFragment; +import im.actor.sdk.controllers.conversation.attach.ShareMenuField; + +public class AttachFragmentEx extends AttachFragment { + + public AttachFragmentEx(Peer peer) { + super(peer); + } + + public AttachFragmentEx() { + } + + @Override + protected List onCreateFields() { + List res = super.onCreateFields(); + res.add(new ShareMenuField(R.id.share_test, R.drawable.ic_edit_white_24dp, ActorSDK.sharedActor().style.getAccentColor(), "lol")); + return res; + } + + @Override + protected void onItemClicked(int id) { + super.onItemClicked(id); + if (id == R.id.share_test) { + Toast.makeText(getContext(), "Hey", Toast.LENGTH_LONG).show(); + } + } +} diff --git a/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/fragments/RootFragmentEx.java b/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/fragments/RootFragmentEx.java new file mode 100644 index 0000000000..4af9246bfe --- /dev/null +++ b/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/fragments/RootFragmentEx.java @@ -0,0 +1,15 @@ +package im.actor.fragments; + +import android.support.v7.app.ActionBar; + +import im.actor.develop.R; +import im.actor.sdk.controllers.root.RootFragment; + +public class RootFragmentEx extends RootFragment { + @Override + public void onConfigureActionBar(ActionBar actionBar) { + super.onConfigureActionBar(actionBar); + actionBar.setDisplayShowHomeEnabled(true); + actionBar.setIcon(R.drawable.ic_app_notify); + } +} diff --git a/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/holders/BubbleTextHolderLayouter.java b/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/holders/BubbleTextHolderLayouter.java new file mode 100644 index 0000000000..5ead5160b8 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/holders/BubbleTextHolderLayouter.java @@ -0,0 +1,51 @@ +package im.actor.holders; + +import android.graphics.Color; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import im.actor.core.entity.Message; +import im.actor.core.entity.Peer; +import im.actor.core.entity.content.AbsContent; +import im.actor.core.entity.content.TextContent; +import im.actor.develop.R; +import im.actor.sdk.controllers.conversation.messages.BubbleLayouter; +import im.actor.sdk.controllers.conversation.messages.MessagesAdapter; +import im.actor.sdk.controllers.conversation.messages.content.MessageHolder; +import im.actor.sdk.controllers.conversation.messages.content.AbsMessageViewHolder; +import im.actor.sdk.controllers.conversation.messages.content.preprocessor.PreprocessedData; +import im.actor.sdk.controllers.conversation.view.BubbleContainer; + +public class BubbleTextHolderLayouter implements BubbleLayouter { + + @Override + public boolean isMatch(AbsContent content) { + return content instanceof TextContent; + } + + @Override + public AbsMessageViewHolder onCreateViewHolder(MessagesAdapter adapter, ViewGroup root, Peer peer) { + TextView itemView = new TextView(root.getContext()); + itemView.setId(R.id.text); + BubbleContainer container = new BubbleContainer(root.getContext()); + container.addView(itemView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + return new TextHolderEx(adapter, container); + } + + private class TextHolderEx extends MessageHolder { + TextView tv; + + public TextHolderEx(MessagesAdapter adapter, View itemView) { + super(adapter, itemView, false); + tv = (TextView) container.findViewById(R.id.text); + tv.setTextColor(Color.RED); + } + + @Override + protected void bindData(Message message, long readDate, long receiveDate, boolean isUpdated, PreprocessedData preprocessedData) { + TextContent content = (TextContent) message.getContent(); + tv.setText(content.getText()); + } + } +} diff --git a/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/holders/TextHolderLayouter.java b/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/holders/TextHolderLayouter.java new file mode 100644 index 0000000000..d2aefa0eba --- /dev/null +++ b/actor-sdk/sdk-core-android/android-app/src/main/java/im/actor/holders/TextHolderLayouter.java @@ -0,0 +1,48 @@ +package im.actor.holders; + +import android.graphics.Color; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import im.actor.core.entity.Message; +import im.actor.core.entity.Peer; +import im.actor.core.entity.content.AbsContent; +import im.actor.core.entity.content.TextContent; +import im.actor.sdk.controllers.conversation.messages.BubbleLayouter; +import im.actor.sdk.controllers.conversation.messages.MessagesAdapter; +import im.actor.sdk.controllers.conversation.messages.content.AbsMessageViewHolder; +import im.actor.sdk.controllers.conversation.messages.content.preprocessor.PreprocessedData; + +public class TextHolderLayouter implements BubbleLayouter { + @Override + public boolean isMatch(AbsContent content) { + return content instanceof TextContent; + } + + @Override + public AbsMessageViewHolder onCreateViewHolder(MessagesAdapter adapter, ViewGroup root, Peer peer) { + return new TextHolderEx(new TextView(root.getContext())); + } + + private class TextHolderEx extends AbsMessageViewHolder { + TextView tv; + + public TextHolderEx(View itemView) { + super(itemView); + tv = (TextView) itemView; + tv.setTextColor(Color.RED); + } + + @Override + public void bindData(Message message, Message prev, Message next, long readDate, long receiveDate, PreprocessedData preprocessedData) { + TextContent content = (TextContent) message.getContent(); + tv.setText(content.getText()); + } + + @Override + public void unbind() { + + } + } +} diff --git a/actor-sdk/sdk-core-android/android-google-maps/build.gradle b/actor-sdk/sdk-core-android/android-google-maps/build.gradle index 1c803cff8c..c2ff47223d 100644 --- a/actor-sdk/sdk-core-android/android-google-maps/build.gradle +++ b/actor-sdk/sdk-core-android/android-google-maps/build.gradle @@ -1,9 +1,10 @@ buildscript { dependencies { - classpath 'com.android.tools.build:gradle:2.1.2' + classpath 'com.android.tools.build:gradle:2.1.3' classpath 'com.github.dcendents:android-maven-plugin:1.2' classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.5.3" classpath 'me.tatarka:gradle-retrolambda:3.2.5' + classpath "com.google.gms:google-services:3.0.0" } } plugins { @@ -18,6 +19,8 @@ apply plugin: 'me.tatarka.retrolambda' group = 'im.actor' version = '0.0.1' +def baseVersion = "3.0" + android { compileSdkVersion 24 buildToolsVersion "24.0.0" @@ -50,7 +53,7 @@ dependencies { //compile 'im.actor:android-sdk:0.1.30' compile project(':actor-sdk:sdk-core-android:android-sdk') - compile 'com.google.android.gms:play-services-maps:8.4.0' + compile 'com.google.android.gms:play-services-maps:9.4.0' } @@ -110,7 +113,7 @@ if (project.rootProject.file('gradle.properties').exists()) { properties.load(project.rootProject.file('gradle.properties').newDataInputStream()) ossrhUsername = properties.getProperty("ossrhUsername", "") ossrhPassword = properties.getProperty("ossrhPassword", "") - version = properties.getProperty("version", "") + version = baseVersion + "." + properties.getProperty("build_index", "") nexusStaging { username ossrhUsername diff --git a/actor-sdk/sdk-core-android/android-google-maps/src/main/java/im/actor/map/MapActivity.java b/actor-sdk/sdk-core-android/android-google-maps/src/main/java/im/actor/map/MapActivity.java index 6ed714b379..9e95b2b3bd 100644 --- a/actor-sdk/sdk-core-android/android-google-maps/src/main/java/im/actor/map/MapActivity.java +++ b/actor-sdk/sdk-core-android/android-google-maps/src/main/java/im/actor/map/MapActivity.java @@ -18,7 +18,7 @@ protected void onCreate(Bundle savedInstanceState) { double latitude = getIntent().getDoubleExtra("latitude", 0); if (savedInstanceState == null) { - showFragment(MapFragment.create(longitude, latitude), false, false); + showFragment(MapFragment.create(longitude, latitude), false); } } diff --git a/actor-sdk/sdk-core-android/android-google-maps/src/main/java/im/actor/map/MapPickerActivity.java b/actor-sdk/sdk-core-android/android-google-maps/src/main/java/im/actor/map/MapPickerActivity.java index f581ca9b2a..12324ee953 100644 --- a/actor-sdk/sdk-core-android/android-google-maps/src/main/java/im/actor/map/MapPickerActivity.java +++ b/actor-sdk/sdk-core-android/android-google-maps/src/main/java/im/actor/map/MapPickerActivity.java @@ -8,7 +8,11 @@ import android.content.pm.PackageManager; import android.location.Location; import android.location.LocationManager; +import android.os.Build; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v13.app.ActivityCompat; +import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.SearchView; import android.util.Log; @@ -50,6 +54,8 @@ public class MapPickerActivity extends AppCompatActivity GoogleMap.OnMapLongClickListener, GoogleMap.OnMarkerClickListener, AbsListView.OnScrollListener { + private static final int PERMISSION_REQ_LOCATION = 0; + private static final String LOG_TAG = "MapPickerActivity"; private GoogleMap mMap; // Might be null if Google Play services APK is not available. private Location currentLocation; @@ -284,12 +290,14 @@ private void setUpMapIfNeeded() { // Do a null check to confirm that we have not already instantiated the map. if (mMap == null) { // Try to obtain the map from the SupportMapFragment. - mMap = ((MapFragment) getFragmentManager().findFragmentById(R.id.map)) - .getMap(); - // Check if we were successful in obtaining the map. - if (mMap != null) { - setUpMap(); - } + ((MapFragment) getFragmentManager().findFragmentById(R.id.map)) + .getMapAsync(googleMap -> { + mMap = googleMap; + if (mMap != null) { + setUpMap(); + } + }); + } } @@ -341,6 +349,14 @@ public boolean onClose() { * This should only be called once and when we are sure that {@link #mMap} is not null. */ private void setUpMap() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQ_LOCATION); + im.actor.runtime.Log.d("Permissions", "MapPickerActivity.setUpMap - no permission :c"); + return; + } + } + LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE); for (String provider : locationManager.getAllProviders()) { currentLocation = locationManager.getLastKnownLocation(provider); @@ -362,6 +378,20 @@ private void setUpMap() { mMap.setOnMarkerClickListener(this); } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == PERMISSION_REQ_LOCATION) { + if (grantResults.length > 0) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + setUpMap(); + } else { + //FIXME: if user checks "Don't ask again" button, in next open of activity, it will be finished without any notice about permissions. Need to show toast or alert dialog (with button which redirects to app info (for granting permission manually)). + finish(); + } + } + } + } + private void fetchPlaces(String query) { mMap.clear(); @@ -418,10 +448,10 @@ private void showItemsOnTheMap(ArrayList array) { markers.put(mapItem.id, mMap.addMarker(new MarkerOptions() - .position(mapItem.getLatLng()) - // .title(mapItem.name) - .draggable(false) - .icon(BitmapDescriptorFactory.fromResource(R.drawable.picker_map_marker)) + .position(mapItem.getLatLng()) + // .title(mapItem.name) + .draggable(false) + .icon(BitmapDescriptorFactory.fromResource(R.drawable.picker_map_marker)) )); } } @@ -436,7 +466,6 @@ public void onMyLocationChange(Location location) { mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(location.getLatitude(), location.getLongitude()), 14)); } this.currentLocation = location; - ; accuranceView.setText(getString(R.string.picker_map_pick_my_accuracy, (int) currentLocation.getAccuracy())); Log.d("Location changed", location.toString()); } @@ -445,7 +474,6 @@ public void onMyLocationChange(Location location) { public void onItemClick(AdapterView adapterView, View view, int position, long l) { MapItem mapItem = (MapItem) adapterView.getItemAtPosition(position); - Intent returnIntent = new Intent(); returnIntent.putExtra("latitude", mapItem.getLatLng().latitude); returnIntent.putExtra("longitude", mapItem.getLatLng().longitude); @@ -481,8 +509,6 @@ public void onMapLongClick(LatLng latLng) { //currentPick.setPosition(geoData); } - - } @Override diff --git a/actor-sdk/sdk-core-android/android-google-push/build.gradle b/actor-sdk/sdk-core-android/android-google-push/build.gradle index d522ad2d21..c00020cbcf 100644 --- a/actor-sdk/sdk-core-android/android-google-push/build.gradle +++ b/actor-sdk/sdk-core-android/android-google-push/build.gradle @@ -1,9 +1,10 @@ buildscript { dependencies { - classpath 'com.android.tools.build:gradle:2.1.2' + classpath 'com.android.tools.build:gradle:2.1.3' classpath 'com.github.dcendents:android-maven-plugin:1.2' classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.5.3" classpath 'me.tatarka:gradle-retrolambda:3.2.5' + classpath "com.google.gms:google-services:3.0.0" } } @@ -16,6 +17,8 @@ apply plugin: 'me.tatarka.retrolambda' group = 'im.actor' version = '0.0.1' +def baseVersion = "3.0" + android { compileSdkVersion 24 buildToolsVersion "24.0.0" @@ -48,7 +51,7 @@ dependencies { //compile 'im.actor:android-sdk:0.1.30' compile project(':actor-sdk:sdk-core-android:android-sdk') - compile 'com.google.android.gms:play-services-gcm:8.4.0' + compile 'com.google.android.gms:play-services-gcm:9.4.0' } // @@ -107,7 +110,7 @@ if (project.rootProject.file('gradle.properties').exists()) { properties.load(project.rootProject.file('gradle.properties').newDataInputStream()) ossrhUsername = properties.getProperty("ossrhUsername", "") ossrhPassword = properties.getProperty("ossrhPassword", "") - version = properties.getProperty("version", "") + version = baseVersion + "." + properties.getProperty("build_index", "") nexusStaging { username ossrhUsername diff --git a/actor-sdk/sdk-core-android/android-google-push/src/main/java/im/actor/push/PushManager.java b/actor-sdk/sdk-core-android/android-google-push/src/main/java/im/actor/push/PushManager.java index 0a7673f491..ad1c5637ec 100644 --- a/actor-sdk/sdk-core-android/android-google-push/src/main/java/im/actor/push/PushManager.java +++ b/actor-sdk/sdk-core-android/android-google-push/src/main/java/im/actor/push/PushManager.java @@ -6,7 +6,7 @@ import java.io.IOException; -import im.actor.core.util.ExponentialBackoff; +import im.actor.runtime.util.ExponentialBackoff; import im.actor.runtime.Log; import im.actor.sdk.ActorSDK; import im.actor.sdk.core.ActorPushManager; diff --git a/actor-sdk/sdk-core-android/android-google-push/src/main/java/im/actor/push/PushReceiver.java b/actor-sdk/sdk-core-android/android-google-push/src/main/java/im/actor/push/PushReceiver.java index a002f0f48c..a6eb644c1a 100644 --- a/actor-sdk/sdk-core-android/android-google-push/src/main/java/im/actor/push/PushReceiver.java +++ b/actor-sdk/sdk-core-android/android-google-push/src/main/java/im/actor/push/PushReceiver.java @@ -25,8 +25,9 @@ public void onReceive(Context context, Intent intent) { ActorSDK.sharedActor().waitForReady(); if (extras.containsKey("seq")) { int seq = Integer.parseInt(extras.getString("seq")); + int authId = Integer.parseInt(extras.getString("authId", "0")); Log.d(TAG, "Push received #" + seq); - ActorSDK.sharedActor().getMessenger().onPushReceived(seq); + ActorSDK.sharedActor().getMessenger().onPushReceived(seq, authId); setResultCode(Activity.RESULT_OK); } else if (extras.containsKey("callId")) { long callId = Long.parseLong(extras.getString("callId")); diff --git a/actor-sdk/sdk-core-android/android-sdk/build.gradle b/actor-sdk/sdk-core-android/android-sdk/build.gradle index 568d657b1c..787550856c 100644 --- a/actor-sdk/sdk-core-android/android-sdk/build.gradle +++ b/actor-sdk/sdk-core-android/android-sdk/build.gradle @@ -1,6 +1,6 @@ buildscript { dependencies { - classpath 'com.android.tools.build:gradle:2.1.2' + classpath 'com.android.tools.build:gradle:2.1.3' classpath 'com.github.dcendents:android-maven-plugin:1.2' classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.5.3" classpath 'me.tatarka:gradle-retrolambda:3.2.5' @@ -16,6 +16,8 @@ apply plugin: 'me.tatarka.retrolambda' group = 'im.actor' version = '0.0.1' +def baseVersion = "3.0" + android { compileSdkVersion 24 @@ -75,6 +77,7 @@ dependencies { compile 'com.facebook.fresco:webpsupport:0.10.0' // Not Enabling animated WebP yet compile 'com.github.castorflex.smoothprogressbar:library-circular:1.2.0' compile 'com.facebook.rebound:rebound:0.3.8' + compile 'com.rengwuxian.materialedittext:library:2.1.4' compile 'com.soundcloud.android:android-crop:1.0.0@aar' compile('com.github.chrisbanes.photoview:library:1.2.4') { @@ -157,7 +160,9 @@ if (project.rootProject.file('gradle.properties').exists()) { properties.load(project.rootProject.file('gradle.properties').newDataInputStream()) ossrhUsername = properties.getProperty("ossrhUsername", "") ossrhPassword = properties.getProperty("ossrhPassword", "") - version = properties.getProperty("version", "") + version = baseVersion + "." + properties.getProperty("build_index", "") + + print("##teamcity[buildNumber '$version']") nexusStaging { username ossrhUsername diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/AndroidManifest.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/AndroidManifest.xml index 799a9be9e8..16b41d5c31 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/AndroidManifest.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/AndroidManifest.xml @@ -64,54 +64,38 @@ - - - + android:windowSoftInputMode="adjustPan"> - - - - - - - - - + + + @@ -145,6 +129,36 @@ android:theme="@style/ProfileActivityTheme" android:windowSoftInputMode="adjustNothing|stateAlwaysHidden" /> + + + + + + + + + + T getDelegatedFragment(ActorIntent delegatedIntent, android.support.v } - /** - * Method is used internally for getting delegated list ViewHolder for default messages types - */ - public T getDelegatedViewHolder(Class base, OnDelegateViewHolder callback, Object... args) { - T delegated = delegate.getViewHolder(base, args); - if (delegated != null) { - return delegated; - } else { - return callback.onNotDelegated(); - } - } - - /** - * Method is used internally for getting delegated list ViewHolder for custom messages types - */ - public MessageHolder getDelegatedCustomMessageViewHolder(int dataTypeHash, OnDelegateViewHolder callback, MessagesAdapter messagesAdapter, ViewGroup viewGroup) { - MessageHolder delegated = delegate.getCustomMessageViewHolder(dataTypeHash, messagesAdapter, viewGroup); - if (delegated != null) { - return delegated; - } else { - return callback.onNotDelegated(); - } - } public boolean isVideoCallsEnabled() { return videoCallsEnabled; @@ -978,11 +1055,32 @@ public void setVideoCallsEnabled(boolean videoCallsEnabled) { this.videoCallsEnabled = videoCallsEnabled; } - /** - * Used for handling delegated ViewHolders - */ - public interface OnDelegateViewHolder { - T onNotDelegated(); + public void setOnClientPrivacyEnabled(boolean onClientPrivacyEnabled) { + this.onClientPrivacyEnabled = onClientPrivacyEnabled; + } + + public boolean isOnClientPrivacyEnabled() { + return onClientPrivacyEnabled; + } + + public String getInviteDataUrl() { + return inviteDataUrl; + } + public void setInviteDataUrl(String inviteDataUrl) { + this.inviteDataUrl = inviteDataUrl; + } + + + public static void returnToRoot(Context context) { + Intent i; + ActorIntent startIntent = ActorSDK.sharedActor().getDelegate().getStartIntent(); + if (startIntent != null && startIntent instanceof ActorIntentActivity) { + i = ((ActorIntentActivity) startIntent).getIntent(); + } else { + i = new Intent(context, RootActivity.class); + } + i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + context.startActivity(i); } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/ActorSDKDelegate.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/ActorSDKDelegate.java index 7df3d6da75..dc94c1e80e 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/ActorSDKDelegate.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/ActorSDKDelegate.java @@ -2,16 +2,20 @@ import android.net.Uri; import android.support.v4.app.Fragment; -import android.view.ViewGroup; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; + +import im.actor.core.RawUpdatesHandler; import im.actor.core.entity.Peer; -import im.actor.runtime.android.view.BindedViewHolder; +import im.actor.sdk.controllers.conversation.ChatFragment; import im.actor.sdk.controllers.conversation.attach.AbsAttachFragment; -import im.actor.sdk.controllers.conversation.messages.content.MessageHolder; -import im.actor.sdk.controllers.conversation.messages.MessagesAdapter; -import im.actor.sdk.controllers.settings.BaseGroupInfoActivity; +import im.actor.sdk.controllers.conversation.inputbar.InputBarFragment; +import im.actor.sdk.controllers.conversation.mentions.AutocompleteFragment; +import im.actor.sdk.controllers.conversation.messages.BubbleLayouter; +import im.actor.sdk.controllers.conversation.quote.QuoteFragment; +import im.actor.sdk.controllers.dialogs.DialogsDefaultFragment; import im.actor.sdk.intents.ActorIntent; import im.actor.sdk.intents.ActorIntentFragmentActivity; @@ -58,6 +62,22 @@ public interface ActorSDKDelegate { @Nullable Fragment fragmentForProfile(int uid); + /** + * If not null returned, overrides call fragment + * + * @param callId call id + * @return Fragment + */ + @Nullable + Fragment fragmentForCall(long callId); + + /** + * If not null returned, overrides group info fragment + * + * @return Actor Intent + */ + Fragment fragmentForGroupInfo(int gid); + // // Chat @@ -73,36 +93,45 @@ public interface ActorSDKDelegate { AbsAttachFragment fragmentForAttachMenu(Peer peer); /** - * If Not null returned, overrides default toolbar (no-ui) fragment + * If Not null returned, overrides chat fragment * * @param peer peer - * @return Custom Toolbar fragment + * @return Custom chat fragment */ @Nullable - Fragment fragmentForToolbar(Peer peer); + ChatFragment fragmentForChat(Peer peer); + /** + * If Not null returned, overrides chat input fragment + * + * @return Custom chat input fragment + */ + @Nullable + InputBarFragment fragmentForChatInput(); /** - * Override for hacking default messages view holders + * If Not null returned, overrides chat autocomplete fragment * - * @param base base view holder class - * @param args args passed to view holder - * @param base view holder class - * @param return class - * @return hacked view holder + * @return Custom chat autocomplete fragment + * @param peer peer */ - J getViewHolder(Class base, Object... args); + AutocompleteFragment fragmentForAutocomplete(Peer peer); /** - * Override for hacking custom messages view holders + * If Not null returned, overrides chat quote fragment * - * @param dataTypeHash json dataType hash - * @param messagesAdapter adapter to pass to holder - * @param viewGroup ViewGroup to pass to holder - * @return custom view holder + * @return Custom chat quote fragment */ - MessageHolder getCustomMessageViewHolder(int dataTypeHash, MessagesAdapter messagesAdapter, ViewGroup viewGroup); + QuoteFragment fragmentForQuote(); + /** + * If Not null returned, overrides default toolbar (no-ui) fragment + * + * @param peer peer + * @return Custom Toolbar fragment + */ + @Nullable + Fragment fragmentForToolbar(Peer peer); // // Settings @@ -115,12 +144,6 @@ public interface ActorSDKDelegate { */ ActorIntentFragmentActivity getSettingsIntent(); - /** - * If not null returned, overrides group info activity intent - * - * @return Actor Intent - */ - BaseGroupInfoActivity getGroupInfoIntent(int gid); /** * If not null returned, overrides settings activity intent @@ -181,4 +204,20 @@ public interface ActorSDKDelegate { * @return notification sound color */ int getNotificationColor(); + + /** + * If not null returned, overrides raw updates handler actor + * + * @return RawUpdatesHandler actor + */ + RawUpdatesHandler getRawUpdatesHandler(); + + /** + * Override/add new messages view holders + * + * @param layouters default layouters + */ + void configureChatViewHolders(ArrayList layouters); + + DialogsDefaultFragment fragmentForDialogs(); } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/ActorStyle.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/ActorStyle.java index 6dd2be1b46..b774c5cc13 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/ActorStyle.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/ActorStyle.java @@ -2,8 +2,6 @@ import android.graphics.Color; -import im.actor.sdk.util.Screen; - /** * Actor Styling class */ @@ -26,6 +24,23 @@ public void setDefaultBackgrouds(int[] defaultBackgrouds) { } } + private int[] defaultAvatarPlaceholders = new int[]{ + 0xff509dbb, + 0xff52b3cd, + 0xff7e6eeb, + 0xffde5447, + 0xffee7e37, + 0xffed608b, + 0xff6ac53c}; + + public int[] getDefaultAvatarPlaceholders() { + return defaultAvatarPlaceholders; + } + + public void setDefaultAvatarPlaceholders(int[] defaultAvatarPlaceholders) { + this.defaultAvatarPlaceholders = defaultAvatarPlaceholders; + } + ////////////////////////// // COLORS // ////////////////////////// @@ -358,6 +373,16 @@ public void setDialogsTitleColor(int dialogsTitleColor) { this.dialogsTitleColor = dialogsTitleColor; } + private int dialogsTitleSecureColor = 0xff559d44; + + public int getDialogsTitleSecureColor() { + return getColorWithFallback(dialogsTitleSecureColor, getDialogsTitleColor()); + } + + public void setDialogsTitleSecureColor(int dialogsTitleSecureColor) { + this.dialogsTitleSecureColor = dialogsTitleSecureColor; + } + private int dialogsTextColor = 0; public int getDialogsTextColor() { @@ -500,6 +525,16 @@ public void setTextSubheaderInvColor(int textSubheaderInvColor) { this.textSubheaderInvColor = textSubheaderInvColor; } + private int textDangerColor = 0xffe44b4b; + + public int getTextDangerColor() { + return textDangerColor; + } + + public void setTextDangerColor(int textDangerColor) { + this.textDangerColor = textDangerColor; + } + //Settings private int settingsMainTitleColor = 0; @@ -887,6 +922,10 @@ public static int getDarkenArgb(int color, double percent) { return Color.argb(Color.alpha(color), (int) Math.round(Color.red(color) * percent), (int) Math.round(Color.green(color) * percent), (int) Math.round(Color.blue(color) * percent)); } + public static int adjustColorAlpha(int color, int alpha) { + return (alpha << 24) | (color & 0x00ffffff); + } + /** * Get color with fallback to default - if color is 0, returns fallback color * @@ -907,14 +946,14 @@ public int getColorWithFallback(int baseColor, int fallbackColor) { ////////////////////////// // DialogsFragment layout settings - private int dialogsPaddingTop = Screen.dp(8); + private int dialogsPaddingTopDp = 8; - public int getDialogsPaddingTop() { - return dialogsPaddingTop; + public int getDialogsPaddingTopDp() { + return dialogsPaddingTopDp; } - public void setDialogsPaddingTop(int dialogsPaddingTop) { - this.dialogsPaddingTop = dialogsPaddingTop; + public void setDialogsPaddingTopDp(int dialogsPaddingTopDp) { + this.dialogsPaddingTopDp = dialogsPaddingTopDp; } // ContactsFragment layout settings diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/BaseActorSDKDelegate.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/BaseActorSDKDelegate.java index f5a9d3ddda..b4f87298e4 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/BaseActorSDKDelegate.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/BaseActorSDKDelegate.java @@ -3,19 +3,25 @@ import android.net.Uri; import android.provider.Settings; import android.support.v4.app.Fragment; -import android.view.ViewGroup; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; + +import im.actor.core.RawUpdatesHandler; import im.actor.core.entity.Peer; -import im.actor.runtime.android.view.BindedViewHolder; +import im.actor.sdk.controllers.conversation.ChatFragment; import im.actor.sdk.controllers.conversation.attach.AbsAttachFragment; -import im.actor.sdk.controllers.conversation.messages.content.MessageHolder; -import im.actor.sdk.controllers.conversation.messages.MessagesAdapter; -import im.actor.sdk.controllers.settings.BaseGroupInfoActivity; +import im.actor.sdk.controllers.conversation.inputbar.InputBarFragment; +import im.actor.sdk.controllers.conversation.mentions.AutocompleteFragment; +import im.actor.sdk.controllers.conversation.messages.BubbleLayouter; +import im.actor.sdk.controllers.conversation.quote.QuoteFragment; +import im.actor.sdk.controllers.dialogs.DialogsDefaultFragment; import im.actor.sdk.intents.ActorIntent; import im.actor.sdk.intents.ActorIntentFragmentActivity; +import static im.actor.sdk.util.ActorSDKMessenger.messenger; + /** * Base Implementation of Actor SDK Delegate. This class is recommended to subclass instead * of implementing ActorSDKDelegate @@ -49,6 +55,17 @@ public Fragment fragmentForProfile(int uid) { return null; } + @Nullable + @Override + public Fragment fragmentForCall(long callId) { + return null; + } + + @Override + public Fragment fragmentForGroupInfo(int gid) { + return null; + } + @Nullable @Override public AbsAttachFragment fragmentForAttachMenu(Peer peer) { @@ -57,46 +74,59 @@ public AbsAttachFragment fragmentForAttachMenu(Peer peer) { @Nullable @Override - public Fragment fragmentForToolbar(Peer peer) { + public ChatFragment fragmentForChat(Peer peer) { return null; } + @Nullable @Override - public ActorIntentFragmentActivity getSettingsIntent() { + public InputBarFragment fragmentForChatInput() { return null; } @Override - public BaseGroupInfoActivity getGroupInfoIntent(int gid) { + public AutocompleteFragment fragmentForAutocomplete(Peer peer) { return null; } @Override - public ActorIntentFragmentActivity getChatSettingsIntent() { + public QuoteFragment fragmentForQuote() { return null; } + @Nullable @Override - public ActorIntentFragmentActivity getSecuritySettingsIntent() { + public Fragment fragmentForToolbar(Peer peer) { return null; } @Override - public ActorIntent getChatIntent(Peer peer, boolean compose) { + public ActorIntentFragmentActivity getSettingsIntent() { return null; } @Override - public J getViewHolder(Class base, Object[] args) { + public ActorIntentFragmentActivity getChatSettingsIntent() { return null; } @Override - public MessageHolder getCustomMessageViewHolder(int dataTypeHash, MessagesAdapter messagesAdapter, ViewGroup viewGroup) { + public ActorIntentFragmentActivity getSecuritySettingsIntent() { + return null; + } + + @Override + public ActorIntent getChatIntent(Peer peer, boolean compose) { return null; } public Uri getNotificationSoundForPeer(Peer peer) { + + String globalSound = messenger().getPreferences().getString("userNotificationSound_" + peer.getPeerId()); + if (globalSound != null && !globalSound.equals("none")) { + return Uri.parse(globalSound); + } + return getNotificationSound(); } @@ -105,10 +135,33 @@ public int getNotificationColorForPeer(Peer peer) { } public Uri getNotificationSound() { + String globalSound = messenger().getPreferences().getString("globalNotificationSound"); + if (globalSound != null) { + if (globalSound.equals("none")) { + return null; + } else { + return Uri.parse(globalSound); + } + } + return Settings.System.DEFAULT_NOTIFICATION_URI; } public int getNotificationColor() { return ActorSDK.sharedActor().style.getMainColor(); } + + @Override + public RawUpdatesHandler getRawUpdatesHandler() { + return null; + } + + @Override + public void configureChatViewHolders(ArrayList layouters) { + } + + @Override + public DialogsDefaultFragment fragmentForDialogs() { + return null; + } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/ActorBinder.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/ActorBinder.java index 138a04f470..3f594adcb3 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/ActorBinder.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/ActorBinder.java @@ -14,6 +14,8 @@ import im.actor.core.viewmodel.GroupVM; import im.actor.core.viewmodel.UserPresence; import im.actor.core.viewmodel.UserVM; +import im.actor.runtime.mvvm.ValueDoubleListener; +import im.actor.runtime.mvvm.ValueListener; import im.actor.runtime.mvvm.ValueModel; import im.actor.sdk.ActorSDK; import im.actor.sdk.R; @@ -167,7 +169,6 @@ public void onChanged(UserPresence val, Value valueModel) { public Binding bind(final OnChangedListener callback, ValueModel up) { return bind(up, new ValueChangedListener() { - @Override public void onChanged(UserPresence val, Value valueModel) { callback.onChanged(val.getState().equals(UserPresence.State.ONLINE)); @@ -176,29 +177,25 @@ public void onChanged(UserPresence val, Value valueModel) { } public void bind(final TextView textView, final View titleContainer, final GroupVM value) { - bind(value.getPresence(), value.getMembers(), value.isMember(), new ValueTripleChangedListener, Boolean>() { - @Override - public void onChanged(Integer online, Value onlineModel, - HashSet members, Value> membersModel, Boolean isMember, Value isMemberModel) { - if (isMember) { - titleContainer.setVisibility(View.VISIBLE); - if (online <= 0) { - SpannableStringBuilder builder = new SpannableStringBuilder( - messenger().getFormatter().formatGroupMembers(members.size())); - builder.setSpan(new ForegroundColorSpan(0xB7ffffff), 0, builder.length(), - Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - textView.setText(builder); - } else { - SpannableStringBuilder builder = new SpannableStringBuilder( - messenger().getFormatter().formatGroupMembers(members.size()) + ", "); - builder.setSpan(new ForegroundColorSpan(0xB7ffffff), 0, builder.length(), - Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - builder.append(messenger().getFormatter().formatGroupOnline(online)); - textView.setText(builder); - } + bind(value.getPresence(), value.getMembersCount(), value.isMember(), (online, onlineModel, membersCount, membersModel, isMember, isMemberModel) -> { + if (isMember) { + titleContainer.setVisibility(View.VISIBLE); + if (online <= 0) { + SpannableStringBuilder builder = new SpannableStringBuilder( + messenger().getFormatter().formatGroupMembers(membersCount)); + builder.setSpan(new ForegroundColorSpan(0xB7ffffff), 0, builder.length(), + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + textView.setText(builder); } else { - titleContainer.setVisibility(View.GONE); + SpannableStringBuilder builder = new SpannableStringBuilder( + messenger().getFormatter().formatGroupMembers(membersCount) + ", "); + builder.setSpan(new ForegroundColorSpan(0xB7ffffff), 0, builder.length(), + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + builder.append(messenger().getFormatter().formatGroupOnline(online)); + textView.setText(builder); } + } else { + titleContainer.setVisibility(View.GONE); } }); } @@ -234,6 +231,18 @@ public Binding bind(Value value, ValueChangedListener listener) { return b; } + public Binding bind(Value value, ValueListener listener) { + return bind(value, (val, valueModel) -> { + listener.onChanged(val); + }); + } + + public void bind(Value value1, Value value2, ValueDoubleListener listener) { + bind(value1, value2, (val, valueModel, val2, valueModel2) -> { + listener.onChanged(val, val2); + }); + } + public Binding bind(Value value, ValueChangedListener listener, boolean notify) { value.subscribe(listener, notify); Binding b = new Binding(value, listener); @@ -241,27 +250,31 @@ public Binding bind(Value value, ValueChangedListener listener, boolea return b; } - public void bind(Value value, boolean notify, ValueChangedListener listener) { + public Binding bind(Value value, boolean notify, ValueChangedListener listener) { value.subscribe(listener, notify); - bindings.add(new Binding(value, listener)); + Binding binding = new Binding(value, listener); + bindings.add(binding); + return binding; } - public void bind(final Value value1, final Value value2, + public Binding[] bind(final Value value1, final Value value2, final ValueDoubleChangedListener listener) { - bind(value1, false, new ValueChangedListener() { + Binding[] bindings = new Binding[2]; + bindings[0] = bind(value1, false, new ValueChangedListener() { @Override public void onChanged(T val, Value Value) { listener.onChanged(val, Value, value2.get(), value2); } }); - bind(value2, false, new ValueChangedListener() { + bindings[1] = bind(value2, false, new ValueChangedListener() { @Override public void onChanged(V val, Value Value) { listener.onChanged(value1.get(), value1, val, Value); } }); listener.onChanged(value1.get(), value1, value2.get(), value2); + return bindings; } public void bind(final Value value1, final Value value2, final Value value3, diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/BaseFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/BaseFragment.java index 7eff0380a5..2192ea94b3 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/BaseFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/BaseFragment.java @@ -1,7 +1,10 @@ package im.actor.sdk.controllers; +import android.app.Activity; import android.app.ProgressDialog; +import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.FragmentActivity; @@ -14,16 +17,25 @@ import android.widget.ImageView; import android.widget.TextView; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + import im.actor.core.viewmodel.Command; import im.actor.core.viewmodel.CommandCallback; import im.actor.runtime.function.Consumer; +import im.actor.runtime.function.Function; import im.actor.runtime.promise.Promise; +import im.actor.runtime.promise.PromiseFunc; +import im.actor.runtime.promise.PromiseResolver; import im.actor.sdk.ActorSDK; import im.actor.sdk.ActorStyle; import im.actor.sdk.R; +import im.actor.sdk.controllers.tools.MediaPickerCallback; import im.actor.sdk.util.ViewUtils; -public class BaseFragment extends BinderCompatFragment { +public class BaseFragment extends BinderCompatFragment implements MediaPickerCallback { protected final ActorStyle style = ActorSDK.sharedActor().style; @@ -36,6 +48,8 @@ public class BaseFragment extends BinderCompatFragment { private boolean showHome = false; private boolean showCustom = false; + private ArrayList pending = new ArrayList<>(); + public boolean isRootFragment() { return isRootFragment; } @@ -279,25 +293,24 @@ public void onError(Exception e) { }); } - public void execute(Promise promise) { - execute(promise, R.string.progress_common); + public Promise execute(Promise promise) { + return execute(promise, R.string.progress_common); } - public void execute(Promise promise, int title) { + public Promise execute(Promise promise, int title) { final ProgressDialog dialog = ProgressDialog.show(getContext(), "", getString(title), true, false); - promise - .then(new Consumer() { - @Override - public void apply(T t) { - dismissDialog(dialog); - } - }) - .failure(new Consumer() { - @Override - public void apply(Exception e) { - dismissDialog(dialog); - } - }); + promise.then(new Consumer() { + @Override + public void apply(T t) { + dismissDialog(dialog); + } + }).failure(new Consumer() { + @Override + public void apply(Exception e) { + dismissDialog(dialog); + } + }); + return promise; } public View buildRecord(String titleText, String valueText, @@ -329,6 +342,7 @@ public View buildRecord(String titleText, String valueText, int resourceId, bool if (resourceId != 0 && showIcon) { ImageView iconView = (ImageView) recordView.findViewById(R.id.recordIcon); Drawable drawable = DrawableCompat.wrap(getResources().getDrawable(resourceId)); + drawable.mutate(); DrawableCompat.setTint(drawable, style.getSettingsIconColor()); iconView.setImageDrawable(drawable); } @@ -352,8 +366,8 @@ public View buildRecordBig(String valueText, int resourceId, boolean showIcon, b if (resourceId != 0 && showIcon) { ImageView iconView = (ImageView) recordView.findViewById(R.id.recordIcon); - Drawable drawable = DrawableCompat.wrap(getResources().getDrawable(resourceId)); - DrawableCompat.setTint(drawable, style.getSettingsIconColor()); + Drawable drawable = getResources().getDrawable(resourceId); + drawable.mutate().setColorFilter(style.getSettingsIconColor(), PorterDuff.Mode.SRC_IN); iconView.setImageDrawable(drawable); } @@ -377,6 +391,7 @@ public View buildRecordAction(String valueText, int resourceId, boolean showIcon if (resourceId != 0 && showIcon) { ImageView iconView = (ImageView) recordView.findViewById(R.id.recordIcon); Drawable drawable = DrawableCompat.wrap(getResources().getDrawable(resourceId)); + drawable.mutate(); DrawableCompat.setTint(drawable, style.getGroupActionAddIconColor()); iconView.setImageDrawable(drawable); } @@ -403,4 +418,83 @@ public ActionBar getSupportActionBar() { } return null; } + + protected Promise wrap(Promise p) { + WrappedPromise res = new WrappedPromise<>((PromiseFunc) resolver -> p.pipeTo(resolver)); + pending.add(res); + return res; + } + + @Override + public void onPause() { + super.onPause(); + + for (WrappedPromise w : pending) { + w.kill(); + } + pending.clear(); + } + + public void finishActivity() { + Activity a = getActivity(); + if (a != null) { + a.finish(); + } + } + + @Override + public void onUriPicked(Uri uri) { + + } + + @Override + public void onFilesPicked(List paths) { + + } + + @Override + public void onPhotoPicked(String path) { + + } + + @Override + public void onVideoPicked(String path) { + + } + + @Override + public void onPhotoCropped(String path) { + + } + + @Override + public void onContactPicked(String name, List phones, List emails, byte[] avatar) { + + } + + @Override + public void onLocationPicked(double latitude, double longitude, String street, String place) { + + } + + private class WrappedPromise extends Promise { + + private boolean isKilled; + + public WrappedPromise(PromiseFunc executor) { + super(executor); + } + + public void kill() { + isKilled = true; + } + + @Override + protected void invokeDeliver() { + if (isKilled) { + return; + } + super.invokeDeliver(); + } + } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/BinderCompatFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/BinderCompatFragment.java index 9783cc10a6..6ec35f16c4 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/BinderCompatFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/BinderCompatFragment.java @@ -8,6 +8,8 @@ import im.actor.core.viewmodel.UserVM; import im.actor.runtime.mvvm.Value; import im.actor.runtime.mvvm.ValueDoubleChangedListener; +import im.actor.runtime.mvvm.ValueDoubleListener; +import im.actor.runtime.mvvm.ValueListener; import im.actor.sdk.view.avatar.AvatarView; import im.actor.sdk.view.avatar.CoverAvatarView; import im.actor.runtime.mvvm.ValueChangedListener; @@ -35,12 +37,20 @@ public void bind(ValueModel value, ValueChangedListener listener) { BINDER.bind(value, listener); } + public void bind(ValueModel value, ValueListener listener) { + BINDER.bind(value, listener); + } + + public void bind(Value value1, Value value2, ValueDoubleListener listener) { + BINDER.bind(value1, value2, listener); + } + public void bind(ValueModel value, boolean notify, ValueChangedListener listener) { BINDER.bind(value, listener, notify); } - public void bind(ValueModel value1, ValueModel value2, ValueDoubleChangedListener listener) { - BINDER.bind(value1, value2, listener); + public ActorBinder.Binding[] bind(ValueModel value1, ValueModel value2, ValueDoubleChangedListener listener) { + return BINDER.bind(value1, value2, listener); } public void bind(final CoverAvatarView avatarView, final ValueModel avatar) { @@ -96,6 +106,10 @@ public void onDestroyView() { } } + protected ActorBinder getBINDER() { + return BINDER; + } + public void unbind(ActorBinder.Binding b) { BINDER.unbind(b); } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/DisplayListFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/DisplayListFragment.java index 66986e2aab..b207a49b53 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/DisplayListFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/DisplayListFragment.java @@ -2,6 +2,7 @@ import android.app.Activity; import android.support.v7.widget.CustomItemAnimator; +import android.support.v7.widget.DefaultItemAnimator; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -56,7 +57,8 @@ protected void afterViewInflate(View view, BindedDisplayList displayList) { public void setAnimationsEnabled(boolean isEnabled) { if (isEnabled) { - CustomItemAnimator itemAnimator = new CustomItemAnimator(); + DefaultItemAnimator itemAnimator = new DefaultItemAnimator(); + // CustomItemAnimator itemAnimator = new CustomItemAnimator(); itemAnimator.setSupportsChangeAnimations(false); itemAnimator.setMoveDuration(200); itemAnimator.setAddDuration(150); @@ -128,9 +130,9 @@ public void onResume() { @Override public void onCollectionChanged() { if (displayList.getSize() == 0) { - hideView(collection); + hideView(collection, false); } else { - showView(collection); + showView(collection, false); } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/activity/BaseFragmentActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/activity/BaseFragmentActivity.java index 9895722d16..5a88b798b2 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/activity/BaseFragmentActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/activity/BaseFragmentActivity.java @@ -44,11 +44,7 @@ protected void onCreate(Bundle savedInstanceState) { getWindow().setBackgroundDrawable(new ColorDrawable(STYLE.getMainBackgroundColor())); } - public void showFragment(final Fragment fragment, final boolean addToBackStack) { - showFragment(fragment, addToBackStack, false); - } - - public void showFragment(final Fragment fragment, final boolean addToBackStack, final boolean isAnimated) { + public void showFragment(Fragment fragment, boolean addToBackStack) { FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.content_frame, fragment); if (addToBackStack) { @@ -57,7 +53,7 @@ public void showFragment(final Fragment fragment, final boolean addToBackStack, transaction.commit(); } - public void showNextFragment(final Fragment fragment, final boolean addToBackStack, final boolean isAnimated) { + public void showNextFragment(Fragment fragment, boolean addToBackStack) { FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.content_frame, fragment); if (addToBackStack) { diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/AuthActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/AuthActivity.java index 558ca25c5b..6058d95483 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/AuthActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/AuthActivity.java @@ -123,7 +123,7 @@ private void updateState(AuthState state, boolean force) { if (signType == SIGN_TYPE_UP) { updateState(AuthState.SIGN_UP); } else if (signType == SIGN_TYPE_IN) { - showFragment(new SignInFragment(), false, false); + showFragment(new SignInFragment(), false); } break; @@ -131,18 +131,18 @@ private void updateState(AuthState state, boolean force) { if (currentName != null && !currentName.isEmpty()) { startAuth(currentName); } else { - showFragment(new SignUpFragment(), false, false); + showFragment(new SignUpFragment(), false); } break; case AUTH_PHONE: currentAuthType = AUTH_TYPE_PHONE; currentCode = ""; - showFragment(ActorSDK.sharedActor().getDelegatedFragment(ActorSDK.sharedActor().getDelegate().getAuthStartIntent(), new SignPhoneFragment(), BaseAuthFragment.class), false, false); + showFragment(ActorSDK.sharedActor().getDelegatedFragment(ActorSDK.sharedActor().getDelegate().getAuthStartIntent(), new SignPhoneFragment(), BaseAuthFragment.class), false); break; case AUTH_EMAIL: currentCode = ""; currentAuthType = AUTH_TYPE_EMAIL; - showFragment(ActorSDK.sharedActor().getDelegatedFragment(ActorSDK.sharedActor().getDelegate().getAuthStartIntent(), new SignEmailFragment(), BaseAuthFragment.class), false, false); + showFragment(ActorSDK.sharedActor().getDelegatedFragment(ActorSDK.sharedActor().getDelegate().getAuthStartIntent(), new SignEmailFragment(), BaseAuthFragment.class), false); break; case CODE_VALIDATION_PHONE: case CODE_VALIDATION_EMAIL: @@ -153,11 +153,11 @@ private void updateState(AuthState state, boolean force) { args.putBoolean(ValidateCodeFragment.AUTH_TYPE_SIGN, signType == SIGN_TYPE_IN); args.putString("authId", state == AuthState.CODE_VALIDATION_EMAIL ? currentEmail : Long.toString(currentPhone)); signInFragment.setArguments(args); - showFragment(signInFragment, false, false); + showFragment(signInFragment, false); break; case LOGGED_IN: finish(); - startActivity(new Intent(this, RootActivity.class)); + ActorSDK.sharedActor().startAfterLoginActivity(this); break; } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/BaseAuthFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/BaseAuthFragment.java index ba9b162934..def1393158 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/BaseAuthFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/BaseAuthFragment.java @@ -12,19 +12,26 @@ import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.util.Patterns; +import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.widget.EditText; +import android.widget.FrameLayout; import android.widget.TextView; +import android.widget.Toast; import java.util.regex.Pattern; import im.actor.core.entity.AuthRes; import im.actor.core.entity.Sex; +import im.actor.runtime.mtproto.ConnectionEndpointArray; import im.actor.runtime.promise.Promise; import im.actor.sdk.ActorSDK; import im.actor.sdk.R; import im.actor.sdk.controllers.BaseFragment; +import im.actor.sdk.util.Screen; import im.actor.sdk.view.BaseUrlSpan; import im.actor.sdk.view.CustomClicableSpan; @@ -233,7 +240,6 @@ public void onClick(DialogInterface dialog, int which) { builder.setSpan(span, index, index + ppIndex.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } - @Override public boolean onOptionsItemSelected(MenuItem item) { int i = item.getItemId(); @@ -242,6 +248,40 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; } else if (i == R.id.sign_up) { startSignUp(); + return true; + } else if (i == R.id.change_endpoint) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(R.string.auth_change_endpoint); + + final EditText input = new EditText(getActivity()); + input.setText("tcp://"); + input.setSelection(input.getText().length()); + + int padding = Screen.dp(25); + FrameLayout inputContainer = new FrameLayout(getActivity()); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.setMargins(padding, padding, padding, 0); + inputContainer.addView(input, params); + builder.setView(inputContainer); + + builder.setPositiveButton(R.string.dialog_ok, (dialog, which) -> { + try { + messenger().changeEndpoint(input.getText().toString()); + } catch (ConnectionEndpointArray.UnknownSchemeException e) { + Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_LONG).show(); + } + }); + builder.setNegativeButton(R.string.auth_reset_default_endpoint, (dialog, which) -> { + try { + messenger().changeEndpoint(null); + } catch (ConnectionEndpointArray.UnknownSchemeException e) { + e.printStackTrace(); + } + }); + + builder.show(); + input.requestFocus(); + return true; } else { return super.onOptionsItemSelected(item); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/PickCountryActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/PickCountryActivity.java index 5ce842cd2f..049536c68a 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/PickCountryActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/PickCountryActivity.java @@ -12,7 +12,7 @@ protected void onCreate(Bundle savedInstanceState) { getSupportActionBar().setTitle(R.string.auth_phone_country_title); if (savedInstanceState == null) { - showFragment(new PickCountryFragment(), false, false); + showFragment(new PickCountryFragment(), false); } } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/SignInFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/SignInFragment.java index ab6c5bc790..bef363fbc0 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/SignInFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/SignInFragment.java @@ -181,6 +181,10 @@ public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); menu.clear(); getActivity().getMenuInflater().inflate(R.menu.sign_in, menu); + MenuItem item = menu.findItem(R.id.change_endpoint); + if (item != null) { + item.setVisible(ActorSDK.sharedActor().isUseAlternateEndpointsEnabled()); + } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/SignUpFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/SignUpFragment.java index db0f8c53f1..58342c54e4 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/SignUpFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/auth/SignUpFragment.java @@ -8,6 +8,7 @@ import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; @@ -98,5 +99,9 @@ public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); menu.clear(); getActivity().getMenuInflater().inflate(R.menu.sign_up, menu); + MenuItem item = menu.findItem(R.id.change_endpoint); + if (item != null) { + item.setVisible(ActorSDK.sharedActor().isUseAlternateEndpointsEnabled()); + } } } \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/calls/CallActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/calls/CallActivity.java index 88c85b3522..539d7041e4 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/calls/CallActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/calls/CallActivity.java @@ -3,6 +3,7 @@ import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.view.Menu; import android.view.WindowManager; @@ -44,7 +45,11 @@ protected void onCreate(Bundle savedInstanceState) { if (savedInstanceState == null) { callId = getIntent().getLongExtra("callId", -1); - showFragment(new CallFragment(callId), false, false); + Fragment callFragment = ActorSDK.sharedActor().getDelegate().fragmentForCall(callId); + if (callFragment == null) { + callFragment = CallFragment.create(callId); + } + showFragment(callFragment, false); } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/calls/CallFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/calls/CallFragment.java index ad74e8dbf0..ca797701a9 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/calls/CallFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/calls/CallFragment.java @@ -76,93 +76,97 @@ public class CallFragment extends BaseFragment { - private static final int PERMISSIONS_REQUEST_FOR_CALL = 147; - private static final int NOTIFICATION_ID = 2; - private static final int TIMER_ID = 1; - - private final ActorBinder ACTIVITY_BINDER = new ActorBinder(); - - private long callId = -1; - private Peer peer; - - private Vibrator v; - private View answerContainer; - private Ringtone ringtone; - private CallVM call; - - private AvatarView avatarView; - private TextView nameTV; - private ActorRef timer; - private TextView statusTV; - private View[] avatarLayers; - private View layer1; - private View layer2; - private View layer3; - - private NotificationManager manager; - private CallState currentState; - private ImageButton endCall; - private View endCallContainer; - private boolean speakerOn = false; - private AudioManager audioManager; - - private RecyclerListView membersList; - - private float dX, dY; - - private TintImageView muteCall; - private TextView muteCallTv; - private TintImageView speaker; - private TextView speakerTV; - private TintImageView videoIcon; - private TextView videoTv; + protected static final int PERMISSIONS_REQUEST_FOR_CALL = 147; + protected static final int NOTIFICATION_ID = 2; + protected static final int TIMER_ID = 1; + + protected final ActorBinder ACTIVITY_BINDER = new ActorBinder(); + + protected long callId = -1; + protected Peer peer; + + protected Vibrator v; + protected View answerContainer; + protected Ringtone ringtone; + protected CallVM call; + + protected AvatarView avatarView; + protected TextView nameTV; + protected ActorRef timer; + protected TextView statusTV; + protected View[] avatarLayers; + protected View layer1; + protected View layer2; + protected View layer3; + + protected NotificationManager manager; + protected CallState currentState; + protected ImageButton endCall; + protected View endCallContainer; + protected boolean speakerOn = false; + protected AudioManager audioManager; + + protected RecyclerListView membersList; + + protected float dX, dY; + + protected TintImageView muteCall; + protected TextView muteCallTv; + protected TintImageView speaker; + protected TextView speakerTV; + protected TintImageView videoIcon; + protected TextView videoTv; // // Video References // - private EglBase eglContext; + protected EglBase eglContext; - private SurfaceViewRenderer localVideoView; - private VideoRenderer localRender; - private boolean isLocalViewConfigured; - private VideoTrack localTrack; + protected SurfaceViewRenderer localVideoView; + protected VideoRenderer localRender; + protected boolean isLocalViewConfigured; + protected VideoTrack localTrack; - private SurfaceViewRenderer remoteVideoView; - private VideoRenderer remoteRender; - private boolean isRemoteViewConfigured; - private VideoTrack remoteTrack; + protected SurfaceViewRenderer remoteVideoView; + protected VideoRenderer remoteRender; + protected boolean isRemoteViewConfigured; + protected VideoTrack remoteTrack; // // Vibrate/tone/wakelock // boolean vibrate = true; - private PowerManager powerManager; - private PowerManager.WakeLock wakeLock; - private int field = 0x00000020; + protected PowerManager powerManager; + protected PowerManager.WakeLock wakeLock; + protected int field = 0x00000020; // // Constructor // + static CallFragment create(long callId) { + CallFragment res = new CallFragment(); + Bundle args = new Bundle(); + args.putLong("call_id", callId); + res.setArguments(args); + return res; + } + public CallFragment() { - manager = (NotificationManager) getActivity().getSystemService(Context.NOTIFICATION_SERVICE); } - public CallFragment(long callId) { - this.callId = callId; + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + this.callId = getArguments().getLong("call_id"); this.call = messenger().getCall(callId); if (call == null) { this.peer = Peer.user(myUid()); } else { this.peer = call.getPeer(); } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { manager = (NotificationManager) getActivity().getSystemService(Context.NOTIFICATION_SERVICE); @@ -305,7 +309,32 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, remoteVideoView = (SurfaceViewRenderer) cont.findViewById(R.id.remote_renderer); - localVideoView = new SurfaceViewRenderer(getActivity()); + localVideoView = new SurfaceViewRenderer(getActivity()) { + private boolean aspectFixed = false; + + @Override + public void renderFrame(VideoRenderer.I420Frame frame) { + if (!aspectFixed) { + aspectFixed = true; + int maxWH = Screen.getWidth() / 3 - Screen.dp(20); + float scale = Math.min(maxWH / (float) frame.width, maxWH / (float) frame.height); + + int destW = (int) (scale * frame.width); + int destH = (int) (scale * frame.height); + + boolean turned = frame.rotationDegree % 90 % 2 == 0; + + localVideoView.post(new Runnable() { + @Override + public void run() { + localVideoView.getLayoutParams().height = turned ? destW : destH; + localVideoView.getLayoutParams().width = turned ? destH : destW; + } + }); + } + super.renderFrame(frame); + } + }; localVideoView.setVisibility(View.INVISIBLE); localVideoView.setZOrderMediaOverlay(true); @@ -389,7 +418,7 @@ public void switchAvatarMembers() { } } - private void startTimer() { + protected void startTimer() { final DateFormat formatter = new SimpleDateFormat("HH:mm:ss"); formatter.setTimeZone(TimeZone.getTimeZone("UTC")); @@ -421,7 +450,7 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in } } - private void initIncoming() { + protected void initIncoming() { getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | @@ -449,7 +478,7 @@ private void initIncoming() { }).start(); } - private void onAnswer() { + protected void onAnswer() { endCallContainer.setVisibility(View.VISIBLE); answerContainer.setVisibility(View.GONE); @@ -460,7 +489,7 @@ private void onAnswer() { messenger().answerCall(callId); } - private void doEndCall() { + protected void doEndCall() { messenger().endCall(callId); onCallEnd(); } @@ -778,7 +807,7 @@ public void onPause() { class CallMembersAdapter extends HolderAdapter { - private ArrayList members; + protected ArrayList members; protected CallMembersAdapter(Context context, final ValueModel> members) { super(context); @@ -810,12 +839,12 @@ protected ViewHolder createHolder(CallMember obj) { return new MemberHolder(); } - private class MemberHolder extends ViewHolder { + protected class MemberHolder extends ViewHolder { CallMember data; - private TextView userName; - private TextView status; - private AvatarView avatarView; + protected TextView userName; + protected TextView status; + protected AvatarView avatarView; @Override public View init(final CallMember data, ViewGroup viewGroup, Context context) { diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/ComposeActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/ComposeActivity.java index fee494ed83..ce80f5e17b 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/ComposeActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/ComposeActivity.java @@ -11,7 +11,7 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState == null) { - showFragment(new ComposeFragment(), false, false); + showFragment(new ComposeFragment(), false); } } } \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/ComposeFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/ComposeFragment.java index c116e82271..58b0245299 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/ComposeFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/ComposeFragment.java @@ -23,7 +23,15 @@ protected void addFootersAndHeaders() { addFooterOrHeaderAction(ActorSDK.sharedActor().style.getActionShareColor(), R.drawable.ic_group_white_24dp, R.string.main_fab_new_group, false, () -> { - startActivity(new Intent(getActivity(), CreateGroupActivity.class)); + startActivity(new Intent(getActivity(), CreateGroupActivity.class) + .putExtra(CreateGroupActivity.EXTRA_IS_CHANNEL, false)); + getActivity().finish(); + }, true); + + addFooterOrHeaderAction(ActorSDK.sharedActor().style.getActionShareColor(), + R.drawable.ic_megaphone_18dp_black, R.string.main_fab_new_channel, false, () -> { + startActivity(new Intent(getActivity(), CreateGroupActivity.class) + .putExtra(CreateGroupActivity.EXTRA_IS_CHANNEL, true)); getActivity().finish(); }, true); } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/CreateGroupActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/CreateGroupActivity.java index 11e80ed94e..1995ba5bc7 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/CreateGroupActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/CreateGroupActivity.java @@ -7,12 +7,15 @@ public class CreateGroupActivity extends BaseFragmentActivity { + public static String EXTRA_IS_CHANNEL = "is_channel"; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState == null) { - showFragment(new GroupNameFragment(), false, false); + showFragment(new GroupNameFragment(getIntent().getBooleanExtra(EXTRA_IS_CHANNEL, false)), + false); } } } \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/GroupNameFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/GroupNameFragment.java index e20fb1821e..9f3f5a9354 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/GroupNameFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/GroupNameFragment.java @@ -3,7 +3,6 @@ import android.app.Activity; import android.content.Intent; import android.os.Bundle; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -18,17 +17,19 @@ import im.actor.sdk.R; import im.actor.sdk.controllers.Intents; import im.actor.sdk.controllers.BaseFragment; +import im.actor.sdk.controllers.group.GroupTypeFragment; import im.actor.sdk.util.Screen; import im.actor.sdk.view.avatar.AvatarView; import im.actor.sdk.util.KeyboardHelper; -/** - * Created by ex3ndr on 04.10.14. - */ +import static im.actor.sdk.util.ActorSDKMessenger.messenger; + public class GroupNameFragment extends BaseFragment { private static final int REQUEST_AVATAR = 1; + private boolean isChannel; + private EditText groupName; private AvatarView avatarView; @@ -38,28 +39,56 @@ public class GroupNameFragment extends BaseFragment { public GroupNameFragment() { setRootFragment(true); - setTitle(R.string.create_group_title); setHomeAsUp(true); } + public GroupNameFragment(boolean isChannel) { + this(); + Bundle args = new Bundle(); + args.putBoolean("isChannel", isChannel); + setArguments(args); + } + + @Override + public void onCreate(Bundle saveInstance) { + super.onCreate(saveInstance); + this.isChannel = getArguments().getBoolean("isChannel"); + + if (isChannel) { + setTitle(R.string.create_channel_title); + } else { + setTitle(R.string.create_group_title); + } + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { helper = new KeyboardHelper(getActivity()); View res = inflater.inflate(R.layout.fragment_create_group_name, container, false); res.setBackgroundColor(ActorSDK.sharedActor().style.getMainBackgroundColor()); - ((TextView) res.findViewById(R.id.create_group_hint)).setTextColor(ActorSDK.sharedActor().style.getTextSecondaryColor()); + + TextView hintTextView = (TextView) res.findViewById(R.id.create_group_hint); + hintTextView.setTextColor(ActorSDK.sharedActor().style.getTextSecondaryColor()); + if (isChannel) { + hintTextView.setText(R.string.create_channel_hint); + } else { + hintTextView.setText(R.string.create_group_hint); + } + groupName = (EditText) res.findViewById(R.id.groupTitle); - groupName.setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - if (actionId == EditorInfo.IME_ACTION_NEXT) { - next(); - return true; - } - return false; + groupName.setOnEditorActionListener((v, actionId, event) -> { + if (actionId == EditorInfo.IME_ACTION_NEXT) { + next(); + return true; } + return false; }); + if (isChannel) { + groupName.setHint(R.string.create_channel_name_hint); + } else { + groupName.setHint(R.string.create_group_name_hint); + } groupName.setTextColor(ActorSDK.sharedActor().style.getTextPrimaryColor()); groupName.setHintTextColor(ActorSDK.sharedActor().style.getTextHintColor()); @@ -69,11 +98,8 @@ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { // avatarView.getHierarchy().setControllerOverlay(getResources().getDrawable(R.drawable.circle_selector)); avatarView.setImageURI(null); - res.findViewById(R.id.pickAvatar).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivityForResult(Intents.pickAvatar(avatarPath != null, getActivity()), REQUEST_AVATAR); - } + res.findViewById(R.id.pickAvatar).setOnClickListener(view -> { + startActivityForResult(Intents.pickAvatar(avatarPath != null, getActivity()), REQUEST_AVATAR); }); return res; @@ -104,8 +130,15 @@ public boolean onOptionsItemSelected(MenuItem item) { private void next() { String title = groupName.getText().toString().trim(); if (title.length() > 0) { - ((CreateGroupActivity) getActivity()).showNextFragment( - GroupUsersFragment.create(groupName.getText().toString().trim(), avatarPath), false, true); + if (isChannel) { + execute(messenger().createChannel(groupName.getText().toString().trim(), avatarPath).then(gid -> { + ((CreateGroupActivity) getActivity()).showNextFragment( + GroupTypeFragment.create(gid, true), false); + })); + } else { + ((CreateGroupActivity) getActivity()).showNextFragment( + GroupUsersFragment.createGroup(groupName.getText().toString().trim(), avatarPath), false); + } } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/GroupUsersFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/GroupUsersFragment.java index 4141245c09..fd23720cfc 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/GroupUsersFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/compose/GroupUsersFragment.java @@ -18,12 +18,15 @@ import im.actor.core.entity.Contact; import im.actor.core.viewmodel.CommandCallback; +import im.actor.runtime.function.Consumer; +import im.actor.runtime.promise.Promise; import im.actor.sdk.ActorSDK; import im.actor.sdk.R; import im.actor.sdk.controllers.Intents; import im.actor.sdk.controllers.compose.view.UserSpan; import im.actor.sdk.controllers.contacts.BaseContactFragment; import im.actor.sdk.util.BoxUtil; +import im.actor.sdk.util.KeyboardHelper; import im.actor.sdk.util.Screen; import static im.actor.sdk.util.ActorSDKMessenger.messenger; @@ -35,16 +38,17 @@ public class GroupUsersFragment extends BaseContactFragment { private String avatarPath; private EditText searchField; private TextWatcher textWatcher; + private boolean isChannel; + private int gid; public GroupUsersFragment() { super(true, false, true); setRootFragment(true); setHomeAsUp(true); - setTitle(R.string.create_group_title); } - public static GroupUsersFragment create(String title, String avatarPath) { + public static GroupUsersFragment createGroup(String title, String avatarPath) { GroupUsersFragment res = new GroupUsersFragment(); Bundle args = new Bundle(); args.putString("title", title); @@ -53,9 +57,21 @@ public static GroupUsersFragment create(String title, String avatarPath) { return res; } + public static GroupUsersFragment createChannel(int gid) { + GroupUsersFragment res = new GroupUsersFragment(); + Bundle args = new Bundle(); + args.putBoolean("isChannel", true); + args.putInt("gid", gid); + res.setArguments(args); + return res; + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + isChannel = getArguments().getBoolean("isChannel", false); + setTitle(isChannel ? R.string.channel_add_members : R.string.create_group_title); + gid = getArguments().getInt("gid"); title = getArguments().getString("title"); avatarPath = getArguments().getString("avatarPath"); @@ -86,6 +102,8 @@ public void afterTextChanged(Editable s) { filter(filter); } }; + KeyboardHelper helper = new KeyboardHelper(getActivity()); + helper.setImeVisibility(searchField, false); return res; } @@ -99,33 +117,47 @@ public void onResume() { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.create_group, menu); - menu.findItem(R.id.done).setEnabled(getSelectedCount() > 0); + menu.findItem(R.id.done).setEnabled(getSelectedCount() > 0 || isChannel); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.done) { - if (getSelectedCount() > 0) { - execute(messenger().createGroup(title, avatarPath, BoxUtil.unbox(getSelected())), - R.string.progress_common, new CommandCallback() { - @Override - public void onResult(Integer res) { - getActivity().startActivity(Intents.openGroupDialog(res, true, getActivity())); - getActivity().finish(); - } - - @Override - public void onError(Exception e) { - Toast.makeText(getActivity(), getString(R.string.toast_unable_create_group), Toast.LENGTH_LONG).show(); - - } - }); + if (isChannel) { + if (getSelectedCount() > 0) { + Promise invites = null; + for (int uid : getSelected()) { + if (invites == null) { + invites = messenger().inviteMemberPromise(gid, uid); + } else { + invites.chain(o -> messenger().inviteMemberPromise(gid, uid)); + } + } + execute(invites.then(o -> openChannel()), R.string.progress_common); + } else { + openChannel(); + } + } else { + if (getSelectedCount() > 0) { + execute(messenger().createGroup(title, avatarPath, BoxUtil.unbox(getSelected())).then(gid -> { + getActivity().startActivity(Intents.openGroupDialog(gid, true, getActivity())); + getActivity().finish(); + }).failure(e -> { + Toast.makeText(getActivity(), getString(R.string.toast_unable_create_group), + Toast.LENGTH_LONG).show(); + })); + } } return true; } return super.onOptionsItemSelected(item); } + protected void openChannel() { + getActivity().startActivity(Intents.openGroupDialog(gid, true, getActivity())); + getActivity().finish(); + } + @Override public void onItemClicked(Contact contact) { if (isSelected(contact.getUid())) { diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/contacts/BaseContactFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/contacts/BaseContactFragment.java index d938e4aac8..eaaf6ac1dd 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/contacts/BaseContactFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/contacts/BaseContactFragment.java @@ -70,6 +70,8 @@ protected View onCreateContactsView(int layoutId, LayoutInflater inflater, ViewG } } + setAnimationsEnabled(false); + View headerPadding = new View(getActivity()); headerPadding.setBackgroundColor(ActorSDK.sharedActor().style.getMainBackgroundColor()); headerPadding.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, useCompactVersion ? 0 : ActorSDK.sharedActor().style.getContactsMainPaddingTop())); @@ -77,7 +79,6 @@ protected View onCreateContactsView(int layoutId, LayoutInflater inflater, ViewG addFootersAndHeaders(); - if (emptyView != null) { if (messenger().getAppState().getIsContactsEmpty().get()) { emptyView.setVisibility(View.VISIBLE); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/contacts/ContactsActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/contacts/ContactsActivity.java index c8b46701d1..54de05d097 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/contacts/ContactsActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/contacts/ContactsActivity.java @@ -12,7 +12,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState == null) { - showFragment(new ContactsFragment(), false, false); + showFragment(new ContactsFragment(), false); } } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/contacts/InviteActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/contacts/InviteActivity.java index 52bb42a834..1fda1902a9 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/contacts/InviteActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/contacts/InviteActivity.java @@ -17,7 +17,7 @@ protected void onCreate(Bundle savedInstanceState) { if (savedInstanceState == null) { - showFragment(new InviteFragment(), false, false); + showFragment(new InviteFragment(), false); } } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/ChatActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/ChatActivity.java index ef30244fb0..1e834268d2 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/ChatActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/ChatActivity.java @@ -2,6 +2,8 @@ import android.content.Context; import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; @@ -9,14 +11,27 @@ import android.support.v4.app.Fragment; import android.support.v7.view.ActionMode; import android.support.v7.widget.Toolbar; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; import im.actor.core.entity.Peer; +import im.actor.sdk.ActorSDK; import im.actor.sdk.R; import im.actor.sdk.controllers.activity.BaseActivity; +import im.actor.sdk.util.Screen; +import im.actor.sdk.view.ActorToolbar; public class ChatActivity extends BaseActivity { public static final String EXTRA_CHAT_PEER = "chat_peer"; + private String quote; + private ChatFragment chatFragment; + + private Toolbar toolbar; public static Intent build(Peer peer, Context context) { final Intent intent = new Intent(context, ChatActivity.class); @@ -28,31 +43,93 @@ public static Intent build(Peer peer, Context context) { public void onCreate(Bundle saveInstance) { super.onCreate(saveInstance); + // // For faster keyboard open/close + // + getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); // // Loading Layout // - setContentView(R.layout.activity_dialog); - setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); + + RelativeLayout rootLayout = new RelativeLayout(this); + View antiFocus = new View(this); + antiFocus.setLayoutParams(new RelativeLayout.LayoutParams(0, 0)); + antiFocus.setFocusable(true); + antiFocus.setFocusableInTouchMode(true); + rootLayout.addView(antiFocus); + + FrameLayout chatFragmentCont = new FrameLayout(this); + chatFragmentCont.setId(R.id.chatFragment); + RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + params.addRule(RelativeLayout.BELOW, R.id.toolbar); + chatFragmentCont.setLayoutParams(params); + rootLayout.addView(chatFragmentCont); + + ActorToolbar toolbar = new ActorToolbar(this); + // Toolbar toolbar = new Toolbar(this); + final TypedArray styledAttributes = getTheme().obtainStyledAttributes(new int[]{R.attr.actionBarSize}); + int actionBarSize = (int) styledAttributes.getDimension(0, 0); + styledAttributes.recycle(); + toolbar.setLayoutParams(new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, actionBarSize)); + toolbar.setMinimumHeight(actionBarSize); + toolbar.setId(R.id.toolbar); + toolbar.setBackgroundColor(ActorSDK.sharedActor().style.getToolBarColor()); + toolbar.setItemColor(Color.WHITE); + rootLayout.addView(toolbar); + this.toolbar = toolbar; + + FrameLayout overlay = new FrameLayout(this); + overlay.setId(R.id.overlay); + overlay.setLayoutParams(new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + rootLayout.addView(overlay); + + setContentView(rootLayout); + setSupportActionBar(toolbar); // // Loading Fragments if needed // + if (saveInstance == null) { - Peer peer = Peer.fromUniqueId(getIntent().getExtras().getLong(EXTRA_CHAT_PEER)); - ChatFragment chatFragment = ChatFragment.create(peer); - getSupportFragmentManager().beginTransaction() - .add(R.id.chatFragment, chatFragment) - .commitNow(); - String quote = getIntent().getStringExtra("forward_text_raw"); - if (quote != null) { - chatFragment.onMessageQuote(quote); - } + handleIntent(getIntent()); } } + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + handleIntent(intent); + } + + protected void handleIntent(Intent intent) { + Peer peer = Peer.fromUniqueId(intent.getExtras().getLong(EXTRA_CHAT_PEER)); + if (chatFragment != null) { + getSupportFragmentManager().beginTransaction().remove(chatFragment).commitNow(); + } + chatFragment = ActorSDK.sharedActor().getDelegate().fragmentForChat(peer); + if (chatFragment == null) { + chatFragment = ChatFragment.create(peer); + } + getSupportFragmentManager().beginTransaction() + .add(R.id.chatFragment, chatFragment) + .commitNow(); + quote = intent.getStringExtra("forward_text_raw"); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + final TypedArray styledAttributes = getTheme().obtainStyledAttributes(new int[]{R.attr.actionBarSize}); + int actionBarSize = (int) styledAttributes.getDimension(0, 0); + styledAttributes.recycle(); + toolbar.setLayoutParams(new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, actionBarSize)); + toolbar.setMinimumHeight(actionBarSize); + } @Override public void onBackPressed() { @@ -66,6 +143,15 @@ public void onBackPressed() { } } + @Override + protected void onResume() { + super.onResume(); + if (quote != null) { + chatFragment.onMessageQuote(quote); + quote = null; + } + } + @Override public ActionMode startSupportActionMode(@NonNull final ActionMode.Callback callback) { // Fix for bug https://code.google.com/p/android/issues/detail?id=159527 diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/ChatFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/ChatFragment.java index a05dcf54a7..dd8ac7f636 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/ChatFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/ChatFragment.java @@ -1,6 +1,7 @@ package im.actor.sdk.controllers.conversation; import android.app.Activity; +import android.content.res.Configuration; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; @@ -10,6 +11,7 @@ import android.widget.TextView; import android.widget.Toast; +import im.actor.core.entity.GroupType; import im.actor.core.entity.Peer; import im.actor.core.entity.PeerType; import im.actor.core.entity.Sticker; @@ -20,6 +22,7 @@ import im.actor.runtime.mvvm.Value; import im.actor.runtime.mvvm.ValueChangedListener; import im.actor.runtime.mvvm.ValueDoubleChangedListener; +import im.actor.runtime.mvvm.ValueListener; import im.actor.sdk.ActorSDK; import im.actor.sdk.ActorSDKLauncher; import im.actor.sdk.R; @@ -95,13 +98,30 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, if (toolbarFragment == null) { toolbarFragment = new ChatToolbarFragment(peer); } + InputBarFragment inputBarFragment = ActorSDK.sharedActor().getDelegate().fragmentForChatInput(); + if (inputBarFragment == null) { + inputBarFragment = new InputBarFragment(); + } + + AutocompleteFragment autocompleteFragment = ActorSDK.sharedActor().getDelegate().fragmentForAutocomplete(peer); + if (autocompleteFragment == null) { + autocompleteFragment = AutocompleteFragment.create(peer); + autocompleteFragment.setUnderlyingView(res.findViewById(R.id.messagesFragment)); + } + + QuoteFragment quoteFragment = ActorSDK.sharedActor().getDelegate().fragmentForQuote(); + if (quoteFragment == null) { + quoteFragment = new QuoteFragment(); + } + MessagesDefaultFragment messagesDefaultFragment = MessagesDefaultFragment.create(peer); + messagesDefaultFragment.setNewMessageListener(inputBarFragment); getChildFragmentManager().beginTransaction() .add(toolbarFragment, "toolbar") - .add(R.id.messagesFragment, MessagesDefaultFragment.create(peer)) - .add(R.id.sendFragment, new InputBarFragment()) - .add(R.id.quoteFragment, new QuoteFragment()) + .add(R.id.messagesFragment, messagesDefaultFragment) + .add(R.id.sendFragment, inputBarFragment) + .add(R.id.quoteFragment, quoteFragment) .add(R.id.emptyPlaceholder, new EmptyChatPlaceholder()) - .add(R.id.autocompleteContainer, new AutocompleteFragment(peer)) + .add(R.id.autocompleteContainer, autocompleteFragment) .commitNow(); AbsAttachFragment fragment = ActorSDK.sharedActor().getDelegate().fragmentForAttachMenu(peer); @@ -167,15 +187,40 @@ public void onResume() { } } else if (peer.getPeerType() == PeerType.GROUP) { GroupVM groupVM = groups().get(peer.getPeerId()); - - bind(groupVM.isMember(), (val, valueModel) -> { - if (val) { + bind(groupVM.isMember(), groupVM.getIsCanWriteMessage(), (isMember, valueModel, canWriteMessage, valueModel2) -> { + if (canWriteMessage) { goneView(inputOverlayContainer, false); showView(inputContainer, false); + } else if (isMember) { + if (messenger().isNotificationsEnabled(peer)) { + inputOverlayText.setText(getString(R.string.chat_mute)); + } else { + inputOverlayText.setText(getString(R.string.chat_unmute)); + } + inputOverlayText.setTextColor(style.getListActionColor()); + inputOverlayText.setClickable(true); + inputOverlayText.setEnabled(true); + showView(inputOverlayContainer, false); + goneView(inputContainer, false); + } else if (groupVM.getIsCanJoin().get()) { + inputOverlayText.setText(getString(R.string.join)); + inputOverlayText.setTextColor(style.getListActionColor()); + inputOverlayText.setClickable(true); + inputOverlayText.setEnabled(true); + showView(inputOverlayContainer, false); + goneView(inputContainer, false); + } else if (groupVM.getIsDeleted().get()) { + inputOverlayText.setText(groupVM.getGroupType() == GroupType.CHANNEL ? R.string.channel_deleted : R.string.group_deleted); + inputOverlayText.setTextColor(style.getListActionColor()); + inputOverlayText.setClickable(false); + inputOverlayText.setEnabled(false); + showView(inputOverlayContainer, false); + goneView(inputContainer, false); } else { inputOverlayText.setText(R.string.chat_not_member); inputOverlayText.setTextColor(style.getListActionColor()); inputOverlayText.setClickable(false); + inputOverlayText.setEnabled(false); showView(inputOverlayContainer, false); goneView(inputContainer, false); } @@ -193,6 +238,21 @@ public void onOverlayPressed() { execute(messenger().unblockUser(userVM.getId())); } } + } else if (peer.getPeerType() == PeerType.GROUP) { + GroupVM groupVM = groups().get(peer.getPeerId()); + if (groupVM.isMember().get()) { + if (messenger().isNotificationsEnabled(peer)) { + messenger().changeNotificationsEnabled(peer, false); + inputOverlayText.setText(getString(R.string.chat_unmute)); + } else { + messenger().changeNotificationsEnabled(peer, true); + inputOverlayText.setText(getString(R.string.chat_mute)); + } + } else if (groupVM.getIsCanJoin().get()) { + messenger().joinGroup(groupVM.getId()); + } else { + // TODO: Rejoin + } } } @@ -206,6 +266,10 @@ public boolean onBackPressed() { } } + if (findInputBar().onBackPressed()) { + return true; + } + // Message Edit if (editRid != 0) { editRid = 0; @@ -294,7 +358,7 @@ public void onAttachPressed() { AbsAttachFragment attachFragment = findShareFragment(); if (attachFragment != null) { - quoteContainer.post(() -> attachFragment.show()); + quoteContainer.postDelayed(() -> attachFragment.show(), 200); } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/KeyboardLayout.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/KeyboardLayout.java new file mode 100644 index 0000000000..98744ee447 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/KeyboardLayout.java @@ -0,0 +1,79 @@ +package im.actor.sdk.controllers.conversation; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; +import android.widget.RelativeLayout; + +import im.actor.sdk.R; + +public class KeyboardLayout extends FrameLayout { + boolean showInternal = false; + boolean sync = false; + private RelativeLayout container; + private int keyboardHeight; + + public KeyboardLayout(Context context) { + super(context); + } + + public KeyboardLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public KeyboardLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public KeyboardLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (container != null) { + if (!showInternal) { + if (container.getPaddingBottom() != 0) { + container.setPadding(0, 0, 0, 0); + } + sync = showInternal; + } + } + + super.onLayout(changed, left, top, right, bottom); + + if (container != null) { + if (showInternal) { + if (container.getPaddingBottom() != keyboardHeight) { + container.setPadding(0, 0, 0, keyboardHeight); + } + sync = showInternal; + } + } + + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (container == null) { + container = (RelativeLayout) findViewById(R.id.container); + } + } + + public void showInternal(int keyboardHeight) { + showInternal = true; + this.keyboardHeight = keyboardHeight; + sync = false; + } + + public void dismissInternal() { + showInternal = false; + sync = true; + } + + public boolean isSync() { + return sync == showInternal; + } + +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/attach/AttachFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/attach/AttachFragment.java index 7adda58b83..f6142b8b5a 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/attach/AttachFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/attach/AttachFragment.java @@ -14,7 +14,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; -import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Base64; import android.view.Gravity; @@ -22,6 +22,7 @@ import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; +import android.view.animation.TranslateAnimation; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; @@ -41,18 +42,34 @@ import im.actor.sdk.controllers.tools.MediaPickerFragment; import im.actor.sdk.util.SDKFeatures; import im.actor.sdk.util.Screen; +import im.actor.sdk.view.MaterialInterpolator; import im.actor.sdk.view.ShareMenuButtonFactory; +import im.actor.sdk.view.adapters.HeaderViewRecyclerAdapter; import static im.actor.sdk.util.ActorSDKMessenger.messenger; public class AttachFragment extends AbsAttachFragment implements MediaPickerCallback { private static final int PERMISSION_REQ_MEDIA = 11; + public static final int SPAN_COUNT = 4; - private View container; + private FrameLayout root; + private View shareButtons; private FastAttachAdapter fastAttachAdapter; private ImageView menuIconToChange; private TextView menuTitleToChange; + private ImageView menuIconToChangeClone; + private TextView menuTitleToChangeClone; + + private boolean isLoaded = false; + private RecyclerView fastShare; + private View bottomBackground; + private boolean isFastShareFullScreen; + private GridLayoutManager layoutManager; + private int shareIconSize; + private View hideClone; + private int fastShareWidth; + private int spanCount; public AttachFragment(Peer peer) { super(peer); @@ -61,90 +78,132 @@ public AttachFragment(Peer peer) { public AttachFragment() { } + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + hide(); + if (layoutManager != null) { + layoutManager = getGridLayoutManager(); + } + root.removeAllViews(); + isLoaded = false; + } + + protected GridLayoutManager getLayoutManager() { + return layoutManager; + } + @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup fcontainer, @Nullable Bundle savedInstanceState) { if (savedInstanceState == null) { - // Adding Media Picker getChildFragmentManager().beginTransaction() .add(new MediaPickerFragment(), "picker") .commitNow(); } - container = getLayoutInflater(null).inflate(R.layout.share_menu, fcontainer, false); - container.setVisibility(View.INVISIBLE); - container.findViewById(R.id.menu_bg).setBackgroundColor(style.getMainBackgroundColor()); - container.findViewById(R.id.cancelField).setOnClickListener(view -> hide()); + root = new FrameLayout(getContext()) { + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (h != oldh && shareButtons != null) { + shareButtons.getLayoutParams().height = root.getHeight() - Screen.dp(135); + shareButtons.requestLayout(); + } + } + }; + root.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + root.setBackgroundColor(getActivity().getResources().getColor(R.color.dialog_overlay)); + root.setVisibility(View.INVISIBLE); + + isLoaded = false; +// messenger().getGalleryScannerActor().send(new GalleryScannerActor.Show()); +// messenger().getGalleryScannerActor().send(new GalleryScannerActor.Hide()); + + return root; + } + + private void prepareView() { + if (isLoaded) { + return; + } + isLoaded = true; + + shareButtons = getLayoutInflater(null).inflate(R.layout.share_menu, root, false); + fastShare = new RecyclerView(getActivity()); + fastShare.setOverScrollMode(View.OVER_SCROLL_NEVER); + + shareButtons.findViewById(R.id.menu_bg).setBackgroundColor(style.getMainBackgroundColor()); + shareButtons.findViewById(R.id.cancelField).setOnClickListener(view -> hide()); + + // + // Setup appearing hide button + // + isFastShareFullScreen = false; + fastShare.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + boolean visible = layoutManager.findFirstVisibleItemPosition() == 0; + if (isFastShareFullScreen == visible) { + isFastShareFullScreen = !visible; + if (visible) { + hideView(hideClone); + } else { + showView(hideClone); + } + } + } + }); // // Building Menu Fields // ArrayList menuFields = new ArrayList<>(onCreateFields()); // Adding Additional Hide for better UI + ShareMenuField shareMenuFieldHide = new ShareMenuField(R.id.share_hide, R.drawable.attach_hide2, style.getAccentColor(), ""); if (menuFields.size() % 2 != 0) { - menuFields.add(new ShareMenuField(R.id.share_hide, R.drawable.attach_hide2, style.getBackyardBackgroundColor(), "")); + menuFields.add(shareMenuFieldHide); } // // Building Layout // - FrameLayout row = (FrameLayout) container.findViewById(R.id.share_row_one); + FrameLayout row = (FrameLayout) shareButtons.findViewById(R.id.share_row_one); boolean first = true; int menuItemSize = Screen.dp(80); int screenWidth = (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? Screen.getWidth() - : Screen.getHeight()); + : Screen.getWidth()); int distance = screenWidth / (menuFields.size() / 2 + menuFields.size() % 2); int initialMargin = distance / 2 - menuItemSize / 2; int marginFromStart = initialMargin; int secondRowTopMargin = Screen.dp(96); - int shareIconSize = Screen.dp(60); + shareIconSize = Screen.dp(60); View.OnClickListener defaultSendOcl = null; + Configuration config = getResources().getConfiguration(); + boolean isRtl = false; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { + isRtl = config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } + for (int i = 0; i < menuFields.size(); i++) { ShareMenuField f = menuFields.get(i); - LinearLayout shareItem = new LinearLayout(getActivity()); - shareItem.setOrientation(LinearLayout.VERTICAL); - shareItem.setGravity(Gravity.CENTER_HORIZONTAL); - - TextView title = new TextView(getActivity()); - title.setGravity(Gravity.CENTER); - title.setTextColor(ActorSDK.sharedActor().style.getTextSecondaryColor()); - title.setText(f.getTitle()); - title.setTextSize(14); - - ImageView icon = new ImageView(getActivity()); - icon.setClickable(true); - if (f.getSelector() != 0) { - icon.setBackgroundResource(f.getSelector()); - } else { - icon.setBackgroundDrawable(ShareMenuButtonFactory.get(f.getColor(), getActivity())); - icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - icon.setImageResource(f.getIcon()); - } - - shareItem.addView(icon, shareIconSize, shareIconSize); - shareItem.addView(title); - - View.OnClickListener l = v -> { - hide(); - onItemClicked(v.getId()); - }; - icon.setId(f.getId()); - icon.setOnClickListener(l); + View shareItem = instantiateShareMenuItem(f); FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(menuItemSize, menuItemSize); - params.setMargins(marginFromStart, first ? 0 : secondRowTopMargin, initialMargin, 0); + params.setMargins(isRtl ? initialMargin : marginFromStart, first ? 0 : secondRowTopMargin, isRtl ? marginFromStart : initialMargin, 0); if (i == menuFields.size() - 1) { - menuIconToChange = icon; - menuTitleToChange = title; - defaultSendOcl = l; + menuIconToChange = (ImageView) shareItem.getTag(R.id.icon); + menuTitleToChange = (TextView) shareItem.getTag(R.id.title); + defaultSendOcl = (View.OnClickListener) shareItem.getTag(R.id.list); - params.setMargins(marginFromStart, first ? 0 : secondRowTopMargin, 0, 0); + params.setMargins(isRtl ? 0 : marginFromStart, first ? 0 : secondRowTopMargin, isRtl ? marginFromStart : 0, 0); } row.addView(shareItem, params); @@ -154,6 +213,12 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup fcontainer first = !first; } + hideClone = instantiateShareMenuItem(shareMenuFieldHide); + hideClone.setVisibility(View.INVISIBLE); + menuTitleToChangeClone = (TextView) hideClone.getTag(R.id.title); + menuIconToChangeClone = (ImageView) hideClone.getTag(R.id.icon); + menuTitleToChangeClone.setVisibility(View.GONE); + menuIconToChange.setTag(R.id.icon, menuIconToChange.getDrawable()); menuIconToChange.setTag(R.id.background, menuIconToChange.getBackground()); menuTitleToChange.setTag(menuTitleToChange.getText().toString()); @@ -168,11 +233,20 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup fcontainer hide(); }; - RecyclerView fastShare = (RecyclerView) container.findViewById(R.id.fast_share); - fastAttachAdapter = new FastAttachAdapter(getActivity()); - LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false); - fastShare.setAdapter(fastAttachAdapter); +// RecyclerView fastShare = (RecyclerView) shareButtons.findViewById(R.id.fast_share); + fastAttachAdapter = new FastAttachAdapter(getActivity(), () -> fastShareWidth + 1); + + HeaderViewRecyclerAdapter adapter = new HeaderViewRecyclerAdapter(fastAttachAdapter); + adapter.addHeaderView(shareButtons); + layoutManager = getGridLayoutManager(); + fastShare.setAdapter(adapter); fastShare.setLayoutManager(layoutManager); + layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + return position == 0 ? spanCount : 1; + } + }); StateListDrawable background = ShareMenuButtonFactory.get(style.getMainColor(), getActivity()); final View.OnClickListener finalDefaultSendOcl = defaultSendOcl; @@ -184,6 +258,13 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup fcontainer menuTitleToChange.setText(getString(R.string.chat_doc_send) + "(" + val.size() + ")"); menuIconToChange.setOnClickListener(shareSendOcl); menuIconToChange.setPadding(Screen.dp(10), 0, Screen.dp(5), 0); + + + menuIconToChangeClone.setBackgroundDrawable(background); + menuIconToChangeClone.setImageResource(R.drawable.conv_send); + menuIconToChangeClone.setColorFilter(0xffffffff, PorterDuff.Mode.SRC_IN); + menuIconToChangeClone.setOnClickListener(shareSendOcl); + menuIconToChangeClone.setPadding(Screen.dp(10), 0, Screen.dp(5), 0); } else { menuIconToChange.setBackgroundDrawable((Drawable) menuIconToChange.getTag(R.id.background)); @@ -192,15 +273,78 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup fcontainer menuIconToChange.setOnClickListener(finalDefaultSendOcl); menuTitleToChange.setText((String) menuTitleToChange.getTag()); menuIconToChange.setPadding(0, 0, 0, 0); + + menuIconToChangeClone.setBackgroundDrawable((Drawable) menuIconToChange.getTag(R.id.background)); + menuIconToChangeClone.setImageDrawable((Drawable) menuIconToChange.getTag(R.id.icon)); + menuIconToChangeClone.setColorFilter(null); + menuIconToChangeClone.setOnClickListener(finalDefaultSendOcl); + menuIconToChangeClone.setPadding(0, 0, 0, 0); } }); - return container; + shareButtons.getLayoutParams().height = root.getHeight() - Screen.dp(135); + shareButtons.requestLayout(); + + + bottomBackground = new View(getContext()); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(135), Gravity.BOTTOM); + bottomBackground.setBackgroundColor(ActorSDK.sharedActor().style.getMainBackgroundColor()); + root.addView(bottomBackground, params); + root.addView(fastShare); + FrameLayout.LayoutParams params2 = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM | Gravity.RIGHT); + params2.setMargins(0, 0, Screen.dp(20), Screen.dp(20)); + root.addView(hideClone, params2); + } + + @NonNull + private GridLayoutManager getGridLayoutManager() { + spanCount = Screen.getWidth() / Screen.dp(88); + fastShareWidth = Screen.getWidth() / spanCount; + return new GridLayoutManager(getActivity(), spanCount); + } + + private View instantiateShareMenuItem(ShareMenuField f) { + LinearLayout shareItem = new LinearLayout(getActivity()); + shareItem.setOrientation(LinearLayout.VERTICAL); + shareItem.setGravity(Gravity.CENTER_HORIZONTAL); + + TextView title = new TextView(getActivity()); + title.setGravity(Gravity.CENTER); + title.setTextColor(ActorSDK.sharedActor().style.getTextSecondaryColor()); + title.setText(f.getTitle()); + title.setTextSize(14); + + ImageView icon = new ImageView(getActivity()); + icon.setClickable(true); + if (f.getSelector() != 0) { + icon.setBackgroundResource(f.getSelector()); + } else { + icon.setBackgroundDrawable(ShareMenuButtonFactory.get(f.getColor(), getActivity())); + icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + icon.setImageResource(f.getIcon()); + } + + shareItem.addView(icon, shareIconSize, shareIconSize); + shareItem.addView(title); + + View.OnClickListener l = v -> { + hide(); + onItemClicked(v.getId()); + }; + icon.setId(f.getId()); + icon.setOnClickListener(l); + + shareItem.setTag(R.id.title, title); + shareItem.setTag(R.id.icon, icon); + shareItem.setTag(R.id.list, l); + + return shareItem; } @Override public void show() { - if (container.getVisibility() == View.INVISIBLE) { + prepareView(); + if (root.getVisibility() == View.INVISIBLE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Activity activity = getActivity(); if (activity == null) { @@ -215,29 +359,43 @@ public void show() { } onShown(); messenger().getGalleryScannerActor().send(new GalleryScannerActor.Show()); - showView(container); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - View internal = container.findViewById(R.id.menu_bg); - int cx = internal.getWidth() - Screen.dp(56 + 56); - int cy = internal.getHeight() - Screen.dp(56 / 2); - float finalRadius = (float) Math.hypot(cx, cy); - Animator anim = ViewAnimationUtils.createCircularReveal(internal, cx, cy, 0, finalRadius); - anim.setDuration(200); - anim.start(); - internal.setAlpha(1); - } + showView(root); + TranslateAnimation animation = new TranslateAnimation(0, 0, root.getHeight(), 0); + animation.setInterpolator(MaterialInterpolator.getInstance()); + animation.setDuration(200); +// fastShare.startAnimation(animation); +// bottomBackground.startAnimation(animation); + shareButtons.post(new Runnable() { + @Override + public void run() { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + View internal = fastShare; + int cx = internal.getWidth() - Screen.dp(56 + 56); + int cy = internal.getHeight() - Screen.dp(56 / 2); + float finalRadius = (float) Math.hypot(cx, cy); + Animator anim = ViewAnimationUtils.createCircularReveal(internal, cx, cy, 0, finalRadius); + anim.setDuration(200); + anim.start(); + internal.setAlpha(1); + } + } + }); + } } @Override public void hide() { - if (container.getVisibility() == View.VISIBLE) { + if (root != null && root.getVisibility() == View.VISIBLE) { onHidden(); fastAttachAdapter.clearSelected(); messenger().getGalleryScannerActor().send(new GalleryScannerActor.Hide()); - hideView(container); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - View internal = container.findViewById(R.id.menu_bg); + fastShare.scrollToPosition(0); + hideView(root); + + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP && !isFastShareFullScreen) { + View internal = fastShare; int cx = internal.getWidth() - Screen.dp(56 + 56); int cy = internal.getHeight() - Screen.dp(56 / 2); float finalRadius = (float) Math.hypot(cx, cy); @@ -266,6 +424,12 @@ public void onAnimationRepeat(Animator animator) { anim.setDuration(200); anim.start(); + } else { + TranslateAnimation animation = new TranslateAnimation(0, 0, 0, root.getHeight()); + animation.setInterpolator(MaterialInterpolator.getInstance()); + animation.setDuration(250); + fastShare.startAnimation(animation); + bottomBackground.startAnimation(animation); } } } @@ -312,12 +476,12 @@ protected void onItemClicked(int id) { @Override public void onUriPicked(Uri uri) { - execute(messenger().sendUri(getPeer(), uri)); + execute(messenger().sendUri(getPeer(), uri, ActorSDK.sharedActor().getAppName())); } protected void onUrisPicked(List uris) { for (Uri s : uris) { - execute(messenger().sendUri(getPeer(), s)); + execute(messenger().sendUri(getPeer(), s, ActorSDK.sharedActor().getAppName())); } } @@ -349,13 +513,13 @@ public void onContactPicked(String name, List phones, List email } @Override - public void onLocationPicked(double latitude, double longitude, String street, String place) { - messenger().sendLocation(getPeer(), longitude, longitude, street, place); + public void onLocationPicked(double longitude, double latitude, String street, String place) { + messenger().sendLocation(getPeer(), longitude, latitude, street, place); } @Override public boolean onBackPressed() { - if (container.getVisibility() == View.VISIBLE) { + if (root != null && root.getVisibility() == View.VISIBLE) { hide(); return true; } @@ -385,5 +549,10 @@ public void onDestroyView() { fastAttachAdapter.release(); fastAttachAdapter = null; } + shareButtons = null; + fastShare = null; + root = null; + menuIconToChange = null; + menuTitleToChange = null; } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/attach/FastAttachAdapter.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/attach/FastAttachAdapter.java index 9e75d4b78f..562956591b 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/attach/FastAttachAdapter.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/attach/FastAttachAdapter.java @@ -23,6 +23,7 @@ import java.util.Set; import im.actor.runtime.mvvm.ValueModel; +import im.actor.sdk.ActorSDK; import im.actor.sdk.R; import im.actor.sdk.controllers.ActorBinder; import im.actor.sdk.util.Screen; @@ -37,9 +38,12 @@ public class FastAttachAdapter extends RecyclerView.Adapter> selectedVM; private ActorBinder binder; + private WidthGetter widthGetter; - public FastAttachAdapter(Context context) { + public FastAttachAdapter(Context context, WidthGetter widthGetter) { + this.widthGetter = widthGetter; this.context = context; +// setHasStableIds(true); binder = new ActorBinder(); binder.bind(messenger().getGalleryVM().getGalleryMediaPath(), (val, valueModel) -> { imagesPath.clear(); @@ -49,19 +53,23 @@ public FastAttachAdapter(Context context) { selectedVM = new ValueModel<>("fast_share.selected", new HashSet<>()); } + protected View inflate(int id, ViewGroup viewGroup) { return LayoutInflater .from(context) .inflate(id, viewGroup, false); } + public void release() { binder.unbindAll(); } @Override public FastShareVH onCreateViewHolder(ViewGroup parent, int viewType) { - return new FastShareVH(inflate(R.layout.share_menu_fast_share, parent)); + View itemView = inflate(R.layout.share_menu_fast_share, parent); + itemView.setLayoutParams(new ViewGroup.LayoutParams(widthGetter.get(), widthGetter.get())); + return new FastShareVH(itemView); } @Override @@ -82,6 +90,7 @@ public class FastShareVH extends RecyclerView.ViewHolder { public FastShareVH(View itemView) { super(itemView); + itemView.setBackgroundColor(ActorSDK.sharedActor().style.getMainBackgroundColor()); v = (SimpleDraweeView) itemView.findViewById(R.id.image); chb = (CheckBox) itemView.findViewById(R.id.check); int size = Screen.dp(80); @@ -126,4 +135,8 @@ public void clearSelected() { public ValueModel> getSelectedVM() { return selectedVM; } + + public interface WidthGetter { + int get(); + } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/inputbar/InputBarFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/inputbar/InputBarFragment.java index 849de3bc67..b4c8df0f88 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/inputbar/InputBarFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/inputbar/InputBarFragment.java @@ -4,8 +4,10 @@ import android.animation.ObjectAnimator; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; @@ -24,6 +26,7 @@ import android.widget.ImageView; import android.widget.TextView; +import im.actor.core.entity.Message; import im.actor.runtime.Log; import im.actor.runtime.actors.ActorRef; import im.actor.runtime.actors.ActorSystem; @@ -32,6 +35,8 @@ import im.actor.sdk.ActorSDK; import im.actor.sdk.R; import im.actor.sdk.controllers.BaseFragment; +import im.actor.sdk.controllers.conversation.messages.MessagesDefaultFragment; +import im.actor.sdk.controllers.conversation.messages.MessagesFragment; import im.actor.sdk.core.audio.VoiceCaptureActor; import im.actor.sdk.util.KeyboardHelper; import im.actor.sdk.util.Screen; @@ -46,7 +51,7 @@ import static im.actor.sdk.util.ViewUtils.zoomOutView; import static im.actor.sdk.view.emoji.SmileProcessor.emoji; -public class InputBarFragment extends BaseFragment { +public class InputBarFragment extends BaseFragment implements MessagesDefaultFragment.NewMessageListener { private static final int SLIDE_LIMIT = Screen.dp(180); private static final int PERMISSION_REQUEST_RECORD_AUDIO = 1; @@ -88,6 +93,7 @@ public class InputBarFragment extends BaseFragment { // Emoji keyboard protected EmojiKeyboard emojiKeyboard; protected ImageView emojiButton; + private Message lastMessage; @Override public void onCreate(Bundle saveInstance) { @@ -192,8 +198,8 @@ public void afterTextChanged(Editable editable) { // Emoji keyboard // emojiButton = (ImageView) res.findViewById(R.id.ib_emoji); - emojiButton.setOnClickListener(v -> emojiKeyboard.toggle(messageEditText)); - emojiKeyboard = new EmojiKeyboard(getActivity()); + emojiButton.setOnClickListener(v -> emojiKeyboard.toggle()); + emojiKeyboard = getEmojiKeyboard(); emojiKeyboard.setOnStickerClickListener(sticker -> { Fragment parent = getParentFragment(); if (parent instanceof InputBarCallback) { @@ -251,37 +257,22 @@ public void onShow() { return true; }); - voiceRecordActor = ActorSystem.system().actorOf(Props.create(() -> { - return new VoiceCaptureActor(getActivity(), new VoiceCaptureActor.VoiceCaptureCallback() { - @Override - public void onRecordProgress(final long time) { - getActivity().runOnUiThread(() -> { - audioTimer.setText(messenger().getFormatter().formatDuration((int) (time / 1000))); - }); - } + return res; + } - @Override - public void onRecordCrash() { - getActivity().runOnUiThread(() -> { - hideAudio(true); - }); - } + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + if (lastMessage != null) { + onNewMessage(lastMessage); + lastMessage = null; + } - @Override - public void onRecordStop(long progress) { - if (progress < 1200) { - //Cancel - } else { - Fragment parent = getParentFragment(); - if (parent instanceof InputBarCallback) { - ((InputBarCallback) parent).onAudioSent((int) progress, audioFile); - } - } - } - }); - }).changeDispatcher("voice_capture_dispatcher"), "actor/voice_capture"); + } - return res; + @NonNull + protected EmojiKeyboard getEmojiKeyboard() { + return new EmojiKeyboard(getActivity(), messageEditText); } public void requestFocus() { @@ -583,6 +574,39 @@ public void onPause() { voiceRecordActor.send(PoisonPill.INSTANCE); } + @Override + public void onResume() { + super.onResume(); + voiceRecordActor = ActorSystem.system().actorOf(Props.create(() -> new VoiceCaptureActor(getActivity(), new VoiceCaptureActor.VoiceCaptureCallback() { + @Override + public void onRecordProgress(final long time) { + getActivity().runOnUiThread(() -> { + audioTimer.setText(messenger().getFormatter().formatDuration((int) (time / 1000))); + }); + } + + @Override + public void onRecordCrash() { + getActivity().runOnUiThread(() -> { + hideAudio(true); + }); + } + + @Override + public void onRecordStop(long progress) { + if (progress < 1200) { + //Cancel + } else { + Fragment parent = getParentFragment(); + if (parent instanceof InputBarCallback) { + ((InputBarCallback) parent).onAudioSent((int) progress, audioFile); + } + } + } + })).changeDispatcher("voice_capture_dispatcher"), "actor/voice_capture"); + + } + @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); @@ -597,4 +621,28 @@ public void onDestroyView() { emojiKeyboard.release(); emojiKeyboard = null; } + + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (emojiKeyboard != null) { + emojiKeyboard.onConfigurationChange(); + } + } + + public boolean onBackPressed() { + return emojiKeyboard.onBackPressed(); + } + + @Override + public void onNewMessage(Message m) { + if (emojiKeyboard == null) { + // Inputbar fragment not yet created, store last message for later use + lastMessage = m; + } + if (emojiKeyboard instanceof MessagesFragment.NewMessageListener) { + ((MessagesFragment.NewMessageListener) emojiKeyboard).onNewMessage(m); + } + } } \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/mentions/AutocompleteFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/mentions/AutocompleteFragment.java index 57d0eaf3c9..bf19a9a186 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/mentions/AutocompleteFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/mentions/AutocompleteFragment.java @@ -1,16 +1,17 @@ package im.actor.sdk.controllers.conversation.mentions; +import android.graphics.Color; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.view.Gravity; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.Transformation; import android.widget.AbsListView; -import android.widget.AdapterView; import android.widget.FrameLayout; import im.actor.core.entity.BotCommand; @@ -20,6 +21,7 @@ import im.actor.sdk.controllers.BaseFragment; import im.actor.sdk.util.Screen; import im.actor.sdk.view.MaterialInterpolator; +import im.actor.sdk.view.adapters.BottomSheetListView; import im.actor.sdk.view.adapters.HolderAdapter; import im.actor.sdk.view.adapters.RecyclerListView; @@ -32,12 +34,15 @@ public class AutocompleteFragment extends BaseFragment { private boolean isGroup; private HolderAdapter autocompleteAdapter; - private RecyclerListView autocompleteList; + private BottomSheetListView autocompleteList; + private View underlyingView; - public AutocompleteFragment(Peer peer) { + public static AutocompleteFragment create(Peer peer) { + AutocompleteFragment res = new AutocompleteFragment(); Bundle bundle = new Bundle(); bundle.putLong("peer", peer.getUnuqueId()); - setArguments(bundle); + res.setArguments(bundle); + return res; } public AutocompleteFragment() { @@ -61,10 +66,13 @@ public void onCreate(Bundle saveInstance) { @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - autocompleteList = new RecyclerListView(getContext()); + autocompleteList = new BottomSheetListView(getContext()); + autocompleteList.setVisibility(View.INVISIBLE); + autocompleteList.setUnderlyingView(underlyingView); autocompleteList.setDivider(null); autocompleteList.setDividerHeight(0); - autocompleteList.setBackgroundColor(style.getMainBackgroundColor()); + autocompleteList.setBackgroundColor(Color.TRANSPARENT); + if (autocompleteAdapter != null) { autocompleteList.setAdapter(autocompleteAdapter); } @@ -86,13 +94,14 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, }); // Initial zero height - FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); params.gravity = Gravity.BOTTOM; autocompleteList.setLayoutParams(params); return autocompleteList; } + public void onCurrentWordChanged(String text) { if (isBot) { if (text.startsWith("/")) { @@ -133,57 +142,24 @@ public void onDestroyView() { // Expand Animations // - private void expandMentions(final View v, final int oldRowsCount, final int newRowsCount) { - if (newRowsCount == oldRowsCount) { - return; - } - - v.measure(AbsListView.LayoutParams.MATCH_PARENT, AbsListView.LayoutParams.WRAP_CONTENT); - int newRowsHeight = Screen.dp(48) * newRowsCount + newRowsCount; - - final int targetHeight = (newRowsHeight) > Screen.dp(96 + 2) ? Screen.dp(122) : newRowsHeight; - final int initialHeight = v.getLayoutParams().height; + private void expandMentions(final BottomSheetListView list, final int oldRowsCount, final int newRowsCount) { + list.post(() -> { + if (newRowsCount == oldRowsCount) { + return; + } - v.getLayoutParams().height = initialHeight; - v.setVisibility(View.VISIBLE); - Animation a = new ExpandAnimation(v, targetHeight, initialHeight); + list.setMinHeight(newRowsCount == 0 ? 0 : newRowsCount == 1 ? Screen.dp(48) + 1 : newRowsCount == 2 ? Screen.dp(96) + 2 : Screen.dp(122)); + list.setVisibility(View.VISIBLE); +// Animation a = new ExpandAnimation(list, targetHeight, initialHeight); +// +// a.setDuration((newRowsCount > oldRowsCount ? targetHeight : initialHeight / Screen.dp(1))); +// a.setInterpolator(MaterialInterpolator.getInstance()); +// list.startAnimation(a); + }); - a.setDuration((newRowsCount > oldRowsCount ? targetHeight : initialHeight / Screen.dp(1))); - a.setInterpolator(MaterialInterpolator.getInstance()); - v.startAnimation(a); } - private static class ExpandAnimation extends Animation { - - private final View v; - private final int targetHeight; - private final int initialHeight; - private int currentHeight; - - public ExpandAnimation(View v, int targetHeight, int initialHeight) { - this.v = v; - this.targetHeight = targetHeight; - this.initialHeight = initialHeight; - this.currentHeight = initialHeight; - } - - @Override - protected void applyTransformation(float interpolatedTime, Transformation t) { - if (targetHeight > initialHeight) { - currentHeight = - (int) ((targetHeight * interpolatedTime) - initialHeight * interpolatedTime + initialHeight); - } else { - currentHeight = - (int) (initialHeight - (initialHeight * interpolatedTime) - targetHeight * (1f - interpolatedTime) + targetHeight); - } - - v.getLayoutParams().height = currentHeight; - v.requestLayout(); - } - - @Override - public boolean willChangeBounds() { - return true; - } + public void setUnderlyingView(View underlyingView) { + this.underlyingView = underlyingView; } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/mentions/CommandsAdapter.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/mentions/CommandsAdapter.java index fbc8a13675..bf2ade3044 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/mentions/CommandsAdapter.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/mentions/CommandsAdapter.java @@ -34,7 +34,7 @@ public CommandsAdapter(int uid, Context context) { botUser = users().get(uid); highlightColor = context.getResources().getColor(R.color.primary); commands = users().get(uid).getBotCommands().get(); - commandsToShow = new ArrayList<>(commands); + commandsToShow = new ArrayList<>(); this.uid = uid; } @@ -90,6 +90,8 @@ public class CommandHolder extends ViewHolder { @Override public View init(final BotCommand data, ViewGroup viewGroup, Context context) { View res = ((Activity) context).getLayoutInflater().inflate(R.layout.fragment_chat_mention_item, viewGroup, false); + res.setBackgroundColor(ActorSDK.sharedActor().style.getMainBackgroundColor()); + res.findViewById(R.id.container).setBackgroundResource(R.drawable.selector); res.findViewById(R.id.divider).setBackgroundColor(ActorSDK.sharedActor().style.getDividerColor()); commandName = (TextView) res.findViewById(R.id.name); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/mentions/MentionsAdapter.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/mentions/MentionsAdapter.java index 1dfb7f5140..feebe0480d 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/mentions/MentionsAdapter.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/mentions/MentionsAdapter.java @@ -81,6 +81,8 @@ private class GroupViewHolder extends ViewHolder { @Override public View init(final MentionFilterResult data, ViewGroup viewGroup, Context context) { View res = ((Activity) context).getLayoutInflater().inflate(R.layout.fragment_chat_mention_item, viewGroup, false); + res.setBackgroundColor(ActorSDK.sharedActor().style.getMainBackgroundColor()); + res.findViewById(R.id.container).setBackgroundResource(R.drawable.selector); res.findViewById(R.id.divider).setBackgroundColor(ActorSDK.sharedActor().style.getDividerColor()); userName = (TextView) res.findViewById(R.id.name); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/BubbleLayouter.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/BubbleLayouter.java new file mode 100644 index 0000000000..61ea344d0e --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/BubbleLayouter.java @@ -0,0 +1,14 @@ +package im.actor.sdk.controllers.conversation.messages; + +import android.view.ViewGroup; + +import im.actor.core.entity.Peer; +import im.actor.core.entity.content.AbsContent; +import im.actor.sdk.controllers.conversation.messages.content.AbsMessageViewHolder; + +public interface BubbleLayouter { + + boolean isMatch(AbsContent content); + + AbsMessageViewHolder onCreateViewHolder(MessagesAdapter adapter, ViewGroup root, Peer peer); +} \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/DefaultLayouter.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/DefaultLayouter.java new file mode 100644 index 0000000000..b07d37e33e --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/DefaultLayouter.java @@ -0,0 +1,109 @@ +package im.actor.sdk.controllers.conversation.messages; + +import android.view.View; +import android.view.ViewGroup; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; + +import im.actor.core.entity.Peer; +import im.actor.core.entity.content.AbsContent; +import im.actor.core.entity.content.AnimationContent; +import im.actor.core.entity.content.ContactContent; +import im.actor.core.entity.content.DocumentContent; +import im.actor.core.entity.content.LocationContent; +import im.actor.core.entity.content.PhotoContent; +import im.actor.core.entity.content.ServiceContent; +import im.actor.core.entity.content.StickerContent; +import im.actor.core.entity.content.TextContent; +import im.actor.core.entity.content.VideoContent; +import im.actor.core.entity.content.VoiceContent; +import im.actor.sdk.R; +import im.actor.sdk.controllers.conversation.messages.content.AbsMessageViewHolder; +import im.actor.sdk.controllers.conversation.view.BubbleContainer; +import im.actor.sdk.util.ViewUtils; + +public class DefaultLayouter extends LambdaBubbleLayouter { + + public static final int TEXT_HOLDER = 0; + public static final int SERVICE_HOLDER = 1; + public static final int PHOTO_HOLDER = 2; + public static final int VOICE_HOLDER = 4; + public static final int DOCUMENT_HOLDER = 3; + public static final int CONTACT_HOLDER = 5; + public static final int LOCATION_HOLDER = 6; + public static final int STICKER_HOLDER = 7; + + int id; + int layoutId; + + public DefaultLayouter(int holderId, @NotNull ViewHolderCreator creator) { + super(content -> false, creator); + this.id = holderId; + } + + + @Override + public boolean isMatch(AbsContent content) { + for (HolderMapEntry e : holderMap) { + if (e.getaClass().isAssignableFrom(content.getClass())) { + layoutId = e.getLayoutId(); + return e.getId() == id; + } + } + return false; + } + + @Override + public AbsMessageViewHolder onCreateViewHolder(MessagesAdapter adapter, ViewGroup root, Peer peer) { + View view = ViewUtils.inflate(layoutId, root); + if (!(view instanceof BubbleContainer)) { + BubbleContainer container = new BubbleContainer(root.getContext()); + container.addView(view); + view = container; + } + return creator.onCreateViewHolder(adapter, (ViewGroup) view, peer); + } + + private static ArrayList holderMap; + + static { + holderMap = new ArrayList<>(); + holderMap.add(new HolderMapEntry(TextContent.class, TEXT_HOLDER, R.layout.adapter_dialog_text)); + holderMap.add(new HolderMapEntry(ServiceContent.class, SERVICE_HOLDER, R.layout.adapter_dialog_service)); + holderMap.add(new HolderMapEntry(PhotoContent.class, PHOTO_HOLDER, R.layout.adapter_dialog_photo)); + holderMap.add(new HolderMapEntry(VideoContent.class, PHOTO_HOLDER, R.layout.adapter_dialog_photo)); + holderMap.add(new HolderMapEntry(AnimationContent.class, PHOTO_HOLDER, R.layout.adapter_dialog_photo)); + holderMap.add(new HolderMapEntry(VoiceContent.class, VOICE_HOLDER, R.layout.adapter_dialog_audio)); + holderMap.add(new HolderMapEntry(DocumentContent.class, DOCUMENT_HOLDER, R.layout.adapter_dialog_doc)); + holderMap.add(new HolderMapEntry(ContactContent.class, CONTACT_HOLDER, R.layout.adapter_dialog_contact)); + holderMap.add(new HolderMapEntry(LocationContent.class, LOCATION_HOLDER, R.layout.adapter_dialog_locaton)); + holderMap.add(new HolderMapEntry(StickerContent.class, STICKER_HOLDER, R.layout.adapter_dialog_sticker)); + } + + private static class HolderMapEntry { + Class aClass; + int id; + int layoutId; + + public HolderMapEntry(Class aClass, int id, int layoutId) { + this.aClass = aClass; + this.id = id; + this.layoutId = layoutId; + } + + public Class getaClass() { + return aClass; + } + + public int getId() { + return id; + } + + public int getLayoutId() { + return layoutId; + } + } + +} \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/JsonBubbleLayouter.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/JsonBubbleLayouter.java new file mode 100644 index 0000000000..7b33e0bfb2 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/JsonBubbleLayouter.java @@ -0,0 +1,39 @@ +package im.actor.sdk.controllers.conversation.messages; + +import android.view.ViewGroup; + +import org.jetbrains.annotations.NotNull; + +import im.actor.core.entity.Peer; +import im.actor.core.entity.content.AbsContent; +import im.actor.core.entity.content.JsonContent; +import im.actor.runtime.json.JSONException; +import im.actor.runtime.json.JSONObject; +import im.actor.sdk.controllers.conversation.messages.content.AbsMessageViewHolder; + +public class JsonBubbleLayouter extends LambdaBubbleLayouter { + + + public JsonBubbleLayouter(String dataType, @NotNull LambdaBubbleLayouter.ViewHolderCreator creator) { + super(content -> + { + if (content instanceof JsonContent) { + if (dataType == null) { + return true; + } + try { + return dataType.equals(new JSONObject(((JsonContent) content).getRawJson()).getString("dataType")); + } catch (JSONException e) { + e.printStackTrace(); + } + } + return false; + }, creator); + } + + @Override + public boolean isMatch(AbsContent content) { + return matcher.isMatch(content); + } + +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/JsonXmlBubbleLayouter.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/JsonXmlBubbleLayouter.java new file mode 100644 index 0000000000..ccf2e063f5 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/JsonXmlBubbleLayouter.java @@ -0,0 +1,38 @@ +package im.actor.sdk.controllers.conversation.messages; + +import android.support.annotation.LayoutRes; +import android.view.ViewGroup; + +import org.jetbrains.annotations.NotNull; + +import im.actor.core.entity.Peer; +import im.actor.core.entity.content.AbsContent; +import im.actor.core.entity.content.JsonContent; +import im.actor.runtime.json.JSONException; +import im.actor.runtime.json.JSONObject; +import im.actor.sdk.controllers.conversation.messages.content.AbsMessageViewHolder; +import im.actor.sdk.controllers.conversation.view.BubbleContainer; +import im.actor.sdk.util.ViewUtils; + +public class JsonXmlBubbleLayouter extends JsonBubbleLayouter { + + private int id; + + public JsonXmlBubbleLayouter(String dataType, @LayoutRes int id, @NotNull ViewHolderCreator creator) { + super(dataType, creator); + this.id = id; + } + + @Override + public AbsMessageViewHolder onCreateViewHolder(MessagesAdapter adapter, ViewGroup root, Peer peer) { + ViewGroup holder = (ViewGroup) ViewUtils.inflate(id, root); + if (!(holder instanceof BubbleContainer)) { + BubbleContainer rootHolder = new BubbleContainer(root.getContext()); + rootHolder.addView(holder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + holder = rootHolder; + holder.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + } + return creator.onCreateViewHolder(adapter, holder, peer); + } + +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/LambdaBubbleLayouter.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/LambdaBubbleLayouter.java new file mode 100644 index 0000000000..c9266dbcb9 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/LambdaBubbleLayouter.java @@ -0,0 +1,37 @@ +package im.actor.sdk.controllers.conversation.messages; + +import android.view.ViewGroup; + +import org.jetbrains.annotations.NotNull; + +import im.actor.core.entity.Peer; +import im.actor.core.entity.content.AbsContent; +import im.actor.sdk.controllers.conversation.messages.content.AbsMessageViewHolder; + +public class LambdaBubbleLayouter implements BubbleLayouter { + protected Matcher matcher; + protected ViewHolderCreator creator; + + public LambdaBubbleLayouter(@NotNull Matcher matcher, @NotNull ViewHolderCreator creator) { + this.matcher = matcher; + this.creator = creator; + } + + @Override + public boolean isMatch(AbsContent content) { + return matcher.isMatch(content); + } + + @Override + public AbsMessageViewHolder onCreateViewHolder(MessagesAdapter adapter, ViewGroup root, Peer peer) { + return creator.onCreateViewHolder(adapter, root, peer); + } + + public interface Matcher { + boolean isMatch(AbsContent content); + } + + public interface ViewHolderCreator { + AbsMessageViewHolder onCreateViewHolder(MessagesAdapter adapter, ViewGroup root, Peer peer); + } +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/MessagesAdapter.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/MessagesAdapter.java index b88bd3772e..018380a34a 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/MessagesAdapter.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/MessagesAdapter.java @@ -1,69 +1,73 @@ package im.actor.sdk.controllers.conversation.messages; import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; import android.view.ViewGroup; import java.util.HashMap; import im.actor.core.entity.Message; +import im.actor.core.entity.Peer; import im.actor.core.entity.content.AbsContent; -import im.actor.core.entity.content.AnimationContent; -import im.actor.core.entity.content.ContactContent; -import im.actor.core.entity.content.DocumentContent; -import im.actor.core.entity.content.JsonContent; -import im.actor.core.entity.content.LocationContent; -import im.actor.core.entity.content.PhotoContent; -import im.actor.core.entity.content.ServiceContent; -import im.actor.core.entity.content.StickerContent; -import im.actor.core.entity.content.TextContent; -import im.actor.core.entity.content.VideoContent; -import im.actor.core.entity.content.VoiceContent; import im.actor.core.viewmodel.ConversationVM; import im.actor.runtime.generic.mvvm.BindedDisplayList; -import im.actor.runtime.json.JSONException; -import im.actor.runtime.json.JSONObject; import im.actor.runtime.mvvm.Value; import im.actor.runtime.mvvm.ValueChangedListener; import im.actor.sdk.ActorSDK; -import im.actor.sdk.R; import im.actor.runtime.android.view.BindedListAdapter; import im.actor.sdk.controllers.conversation.messages.content.AudioHolder; import im.actor.sdk.controllers.conversation.messages.content.ContactHolder; import im.actor.sdk.controllers.conversation.messages.content.DocHolder; import im.actor.sdk.controllers.conversation.messages.content.LocationHolder; -import im.actor.sdk.controllers.conversation.messages.content.MessageHolder; +import im.actor.sdk.controllers.conversation.messages.content.AbsMessageViewHolder; import im.actor.sdk.controllers.conversation.messages.content.PhotoHolder; import im.actor.sdk.controllers.conversation.messages.content.preprocessor.PreprocessedList; import im.actor.sdk.controllers.conversation.messages.content.ServiceHolder; import im.actor.sdk.controllers.conversation.messages.content.StickerHolder; import im.actor.sdk.controllers.conversation.messages.content.TextHolder; -import im.actor.sdk.controllers.conversation.messages.content.UnsupportedHolder; import im.actor.sdk.controllers.ActorBinder; import static im.actor.sdk.util.ActorSDKMessenger.messenger; -public class MessagesAdapter extends BindedListAdapter { +public class MessagesAdapter extends BindedListAdapter { private MessagesFragment messagesFragment; private ActorBinder BINDER = new ActorBinder(); private Context context; - private long firstUnread = -1; + private long firstUnread = -DefaultLayouter.SERVICE_HOLDER; private long readDate; private long receiveDate; + private Peer peer; + private ViewHolderMatcher matcher; private HashMap selected = new HashMap<>(); + + public MessagesAdapter(final BindedDisplayList displayList, MessagesFragment messagesFragment, Context context) { super(displayList); + matcher = new ViewHolderMatcher(); + + matcher.add(new DefaultLayouter(DefaultLayouter.TEXT_HOLDER, TextHolder::new)); + matcher.add(new DefaultLayouter(DefaultLayouter.SERVICE_HOLDER, ServiceHolder::new)); + matcher.add(new DefaultLayouter(DefaultLayouter.PHOTO_HOLDER, PhotoHolder::new)); + matcher.add(new DefaultLayouter(DefaultLayouter.VOICE_HOLDER, AudioHolder::new)); + matcher.add(new DefaultLayouter(DefaultLayouter.DOCUMENT_HOLDER, DocHolder::new)); + matcher.add(new DefaultLayouter(DefaultLayouter.CONTACT_HOLDER, ContactHolder::new)); + matcher.add(new DefaultLayouter(DefaultLayouter.LOCATION_HOLDER, LocationHolder::new)); + matcher.add(new DefaultLayouter(DefaultLayouter.STICKER_HOLDER, StickerHolder::new)); + + ActorSDK.sharedActor().getDelegate().configureChatViewHolders(matcher.getLayouters()); + + this.messagesFragment = messagesFragment; this.context = context; ConversationVM conversationVM = messenger().getConversationVM(messagesFragment.getPeer()); + peer = messagesFragment.getPeer(); + readDate = conversationVM.getReadDate().get(); receiveDate = conversationVM.getReceiveDate().get(); @@ -152,118 +156,18 @@ public void setFirstUnread(long firstUnread) { @Override public int getItemViewType(int position) { AbsContent content = getItem(position).getContent(); + return matcher.getMatchId(content); - if (content instanceof TextContent) { - return 0; - } else if (content instanceof ServiceContent) { - return 1; - } else if (content instanceof PhotoContent) { - return 2; - } else if (content instanceof AnimationContent) { - return 2; - } else if (content instanceof VideoContent) { - return 2; - } else if (content instanceof VoiceContent) { - return 4; - } else if (content instanceof DocumentContent) { - return 3; - } else if (content instanceof ContactContent) { - return 5; - } else if (content instanceof LocationContent) { - return 6; - } else if (content instanceof StickerContent) { - return 7; - } else if (content instanceof JsonContent) { - try { - String dataType = new JSONObject(((JsonContent) content).getRawJson()).getString("dataType"); - return dataType.hashCode(); - } catch (JSONException e) { - return -1; - } - } - return -1; } - protected View inflate(int id, ViewGroup viewGroup) { - return LayoutInflater - .from(context) - .inflate(id, viewGroup, false); - } @Override - public MessageHolder onCreateViewHolder(final ViewGroup viewGroup, int viewType) { - switch (viewType) { - case 0: - return ActorSDK.sharedActor().getDelegatedViewHolder(TextHolder.class, new ActorSDK.OnDelegateViewHolder() { - @Override - public TextHolder onNotDelegated() { - return new TextHolder(MessagesAdapter.this, inflate(R.layout.adapter_dialog_text, viewGroup)); - } - }, MessagesAdapter.this, inflate(R.layout.adapter_dialog_text, viewGroup)); - case 1: - return ActorSDK.sharedActor().getDelegatedViewHolder(ServiceHolder.class, new ActorSDK.OnDelegateViewHolder() { - @Override - public ServiceHolder onNotDelegated() { - return new ServiceHolder(MessagesAdapter.this, inflate(R.layout.adapter_dialog_service, viewGroup)); - } - }, MessagesAdapter.this, inflate(R.layout.adapter_dialog_service, viewGroup)); - case 2: - return ActorSDK.sharedActor().getDelegatedViewHolder(PhotoHolder.class, new ActorSDK.OnDelegateViewHolder() { - @Override - public PhotoHolder onNotDelegated() { - return new PhotoHolder(MessagesAdapter.this, inflate(R.layout.adapter_dialog_photo, viewGroup)); - } - }, MessagesAdapter.this, inflate(R.layout.adapter_dialog_photo, viewGroup)); - case 3: - return ActorSDK.sharedActor().getDelegatedViewHolder(DocHolder.class, new ActorSDK.OnDelegateViewHolder() { - @Override - public DocHolder onNotDelegated() { - return new DocHolder(MessagesAdapter.this, inflate(R.layout.adapter_dialog_doc, viewGroup)); - } - }, MessagesAdapter.this, inflate(R.layout.adapter_dialog_doc, viewGroup)); - case 4: - return ActorSDK.sharedActor().getDelegatedViewHolder(AudioHolder.class, new ActorSDK.OnDelegateViewHolder() { - @Override - public AudioHolder onNotDelegated() { - return new AudioHolder(MessagesAdapter.this, inflate(R.layout.adapter_dialog_audio, viewGroup)); - } - }, MessagesAdapter.this, inflate(R.layout.adapter_dialog_audio, viewGroup)); - case 5: - return ActorSDK.sharedActor().getDelegatedViewHolder(ContactHolder.class, new ActorSDK.OnDelegateViewHolder() { - @Override - public ContactHolder onNotDelegated() { - return new ContactHolder(MessagesAdapter.this, inflate(R.layout.adapter_dialog_contact, viewGroup)); - } - }, MessagesAdapter.this, inflate(R.layout.adapter_dialog_contact, viewGroup)); - case 6: - return ActorSDK.sharedActor().getDelegatedViewHolder(LocationHolder.class, new ActorSDK.OnDelegateViewHolder() { - @Override - public LocationHolder onNotDelegated() { - return new LocationHolder(MessagesAdapter.this, inflate(R.layout.adapter_dialog_locaton, viewGroup)); - } - }, MessagesAdapter.this, inflate(R.layout.adapter_dialog_locaton, viewGroup)); - case 7: - return ActorSDK.sharedActor().getDelegatedViewHolder(StickerHolder.class, new ActorSDK.OnDelegateViewHolder() { - @Override - public StickerHolder onNotDelegated() { - return new StickerHolder(MessagesAdapter.this, inflate(R.layout.adapter_dialog_sticker, viewGroup)); - } - }, MessagesAdapter.this, inflate(R.layout.adapter_dialog_sticker, viewGroup)); - case -1: - return new UnsupportedHolder(MessagesAdapter.this, inflate(R.layout.adapter_dialog_text, viewGroup)); - default: - return ActorSDK.sharedActor().getDelegatedCustomMessageViewHolder(viewType, new ActorSDK.OnDelegateViewHolder() { - @Override - public MessageHolder onNotDelegated() { - return new UnsupportedHolder(MessagesAdapter.this, inflate(R.layout.adapter_dialog_text, viewGroup)); - } - }, MessagesAdapter.this, viewGroup); - - } + public AbsMessageViewHolder onCreateViewHolder(final ViewGroup viewGroup, int viewType) { + return matcher.onCreateViewHolder(viewType, this, viewGroup, peer); } @Override - public void onBindViewHolder(MessageHolder dialogHolder, int index, Message item) { + public void onBindViewHolder(AbsMessageViewHolder dialogHolder, int index, Message item) { Message prev = null; Message next = null; if (index > 1) { @@ -277,11 +181,12 @@ public void onBindViewHolder(MessageHolder dialogHolder, int index, Message item } @Override - public void onViewRecycled(MessageHolder holder) { + public void onViewRecycled(AbsMessageViewHolder holder) { holder.unbind(); } public ActorBinder getBinder() { return BINDER; } + } \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/MessagesFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/MessagesFragment.java index 2e49070e4c..f1ab43739a 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/MessagesFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/MessagesFragment.java @@ -18,15 +18,12 @@ import im.actor.core.entity.Message; import im.actor.core.entity.Peer; import im.actor.core.viewmodel.ConversationVM; -import im.actor.runtime.mvvm.Value; -import im.actor.runtime.mvvm.ValueChangedListener; -import im.actor.runtime.mvvm.ValueDoubleChangedListener; +import im.actor.runtime.Log; import im.actor.sdk.ActorSDK; import im.actor.sdk.R; -import im.actor.sdk.controllers.conversation.ChatActivity; import im.actor.sdk.controllers.conversation.messages.content.AudioHolder; +import im.actor.sdk.controllers.conversation.messages.content.AbsMessageViewHolder; import im.actor.sdk.controllers.conversation.messages.content.preprocessor.ChatListProcessor; -import im.actor.sdk.controllers.conversation.messages.content.MessageHolder; import im.actor.sdk.controllers.DisplayListFragment; import im.actor.sdk.controllers.settings.BaseActorSettingsFragment; import im.actor.sdk.util.Screen; @@ -35,7 +32,7 @@ import static im.actor.sdk.util.ActorSDKMessenger.messenger; -public abstract class MessagesFragment extends DisplayListFragment { +public abstract class MessagesFragment extends DisplayListFragment { private final boolean isPrimaryMode; @@ -47,6 +44,8 @@ public abstract class MessagesFragment extends DisplayListFragment onCreateAdapter(BindedDisplayList displayList, Activity activity) { + protected BindedListAdapter onCreateAdapter(BindedDisplayList displayList, Activity activity) { messagesAdapter = new MessagesAdapter(displayList, this, activity); if (firstUnread != -1 && messagesAdapter.getFirstUnread() == -1) { messagesAdapter.setFirstUnread(firstUnread); @@ -170,6 +169,7 @@ protected BindedDisplayList onCreateDisplayList() { if (displayList.getListProcessor() == null) { displayList.setListProcessor(new ChatListProcessor(peer, this.getContext())); } + notifyNewMessage(displayList); return displayList; } @@ -179,13 +179,17 @@ protected BindedDisplayList onCreateDisplayList() { // private void recalculateUnreadMessageIfNeeded() { + Log.d("READ_DEBUG", "trying to scroll to unread"); + // Scroll to unread only in primary mode if (!isPrimaryMode) { return; } BindedDisplayList list = getDisplayList(); - firstUnread = conversationVM.getLastMessageDate(); + if (firstUnread == -1) { + firstUnread = conversationVM.getLastReadMessageDate(); + } // Do not scroll to unread twice if (isUnreadLoaded) { @@ -197,11 +201,18 @@ private void recalculateUnreadMessageIfNeeded() { return; } + // refresh list if top message is too old + if (getLastMessage(getDisplayList()).getSortDate() < firstUnread && !reloaded) { + reloaded = true; + getDisplayList().initCenter(firstUnread, true); + return; + } + // If List is not empty: mark as loaded isUnreadLoaded = true; // If don't have unread message date: nothing to do - if (firstUnread == 0) { + if (firstUnread <= 0) { return; } @@ -287,6 +298,17 @@ public void onResume() { public void onCollectionChanged() { super.onCollectionChanged(); recalculateUnreadMessageIfNeeded(); + notifyNewMessage(getDisplayList()); + } + + protected void notifyNewMessage(BindedDisplayList displayList) { + if (newMessageListener != null && displayList.getSize() > 0) { + newMessageListener.onNewMessage(getLastMessage(displayList)); + } + } + + public Message getLastMessage(BindedDisplayList displayList) { + return displayList.getItem(0); } @Override @@ -307,4 +329,12 @@ public void onDestroyView() { messagesAdapter = null; } } + + public void setNewMessageListener(NewMessageListener newMessageListener) { + this.newMessageListener = newMessageListener; + } + + public interface NewMessageListener { + void onNewMessage(Message m); + } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/ViewHolderMatcher.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/ViewHolderMatcher.java new file mode 100644 index 0000000000..7a3b1192a1 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/ViewHolderMatcher.java @@ -0,0 +1,48 @@ +package im.actor.sdk.controllers.conversation.messages; + +import android.view.ViewGroup; + +import java.util.ArrayList; + +import im.actor.core.entity.Peer; +import im.actor.core.entity.content.AbsContent; +import im.actor.sdk.controllers.conversation.messages.content.AbsMessageViewHolder; +import im.actor.sdk.controllers.conversation.messages.content.UnsupportedHolder; +import im.actor.sdk.util.ViewUtils; +import im.actor.sdk.R; + +public class ViewHolderMatcher { + ArrayList layouters = new ArrayList<>(); + + public ViewHolderMatcher add(BubbleLayouter layouter) { + layouters.add(layouter); + return this; + } + + public ViewHolderMatcher addToTop(BubbleLayouter layouter) { + layouters.add(0, layouter); + return this; + } + + + public int getMatchId(AbsContent content) { + for (int i = 0; i < layouters.size(); i++) { + if (layouters.get(i).isMatch(content)) { + return i; + } + } + return -1; + } + + public AbsMessageViewHolder onCreateViewHolder(int id, MessagesAdapter adapter, ViewGroup root, Peer peer) { + if (id == -1) { + return new UnsupportedHolder(adapter, ViewUtils.inflate(R.layout.adapter_dialog_text, root), peer); + } + BubbleLayouter baseViewHolderMatch = layouters.get(id); + return baseViewHolderMatch.onCreateViewHolder(adapter, root, peer); + } + + public ArrayList getLayouters() { + return layouters; + } +} \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/XmlBubbleLayouter.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/XmlBubbleLayouter.java new file mode 100644 index 0000000000..45a8411fbd --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/XmlBubbleLayouter.java @@ -0,0 +1,26 @@ +package im.actor.sdk.controllers.conversation.messages; + +import android.support.annotation.LayoutRes; +import android.view.ViewGroup; + +import org.jetbrains.annotations.NotNull; + +import im.actor.core.entity.Peer; +import im.actor.sdk.controllers.conversation.messages.content.AbsMessageViewHolder; +import im.actor.sdk.util.ViewUtils; + +public class XmlBubbleLayouter extends LambdaBubbleLayouter { + + private int id; + + public XmlBubbleLayouter(@NotNull Matcher matcher, @LayoutRes int id, @NotNull ViewHolderCreator creator) { + super(matcher, creator); + this.id = id; + } + + @Override + public AbsMessageViewHolder onCreateViewHolder(MessagesAdapter adapter, ViewGroup root, Peer peer) { + return creator.onCreateViewHolder(adapter, (ViewGroup) ViewUtils.inflate(id, root), peer); + } + +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/AbsMessageViewHolder.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/AbsMessageViewHolder.java new file mode 100644 index 0000000000..0e2ede23ac --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/AbsMessageViewHolder.java @@ -0,0 +1,17 @@ +package im.actor.sdk.controllers.conversation.messages.content; + +import android.view.View; + +import im.actor.core.entity.Message; +import im.actor.runtime.android.view.BindedViewHolder; +import im.actor.sdk.controllers.conversation.messages.content.preprocessor.PreprocessedData; + +public abstract class AbsMessageViewHolder extends BindedViewHolder { + public AbsMessageViewHolder(View itemView) { + super(itemView); + } + + public abstract void bindData(Message message, Message prev, Message next, long readDate, long receiveDate, PreprocessedData preprocessedData); + + public abstract void unbind(); +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/AudioHolder.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/AudioHolder.java index 99178aae0d..a8d30c66fe 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/AudioHolder.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/AudioHolder.java @@ -17,6 +17,7 @@ import com.droidkit.progress.CircularView; import im.actor.core.entity.Message; +import im.actor.core.entity.Peer; import im.actor.core.entity.content.DocumentContent; import im.actor.core.entity.content.FileLocalSource; import im.actor.core.entity.content.FileRemoteSource; @@ -37,6 +38,7 @@ import im.actor.sdk.core.audio.AudioPlayerActor; import im.actor.sdk.view.TintImageView; +import static im.actor.sdk.util.ActorSDKMessenger.messenger; import static im.actor.sdk.util.ActorSDKMessenger.myUid; import static im.actor.sdk.util.ViewUtils.goneView; import static im.actor.sdk.util.ViewUtils.showView; @@ -70,9 +72,9 @@ public class AudioHolder extends MessageHolder { protected boolean treckingTouch; protected Handler mainThread; - public AudioHolder(MessagesAdapter fragment, final View itemView) { - super(fragment, itemView, false); - context = fragment.getMessagesFragment().getContext(); + public AudioHolder(MessagesAdapter adapter, final View itemView, Peer peer) { + super(adapter, itemView, false); + context = adapter.getMessagesFragment().getContext(); mainThread = new Handler(context.getMainLooper()); waitColor = ActorSDK.sharedActor().style.getConvStatePendingColor(); sentColor = ActorSDK.sharedActor().style.getConvStateSentColor(); @@ -293,7 +295,10 @@ protected void bindData(final Message message, long readDate, long receiveDate, // Resetting progress state if (audioMsg.getSource() instanceof FileRemoteSource) { - boolean autoDownload = audioMsg instanceof VoiceContent; + boolean autoDownload = false; + if (audioMsg instanceof VoiceContent) { + autoDownload = messenger().isAudioAutoDownloadEnabled(); + } downloadFileVM = ActorSDK.sharedActor().getMessenger().bindFile(((FileRemoteSource) audioMsg.getSource()).getFileReference(), autoDownload, new DownloadVMCallback(audioMsg)); } else if (audioMsg.getSource() instanceof FileLocalSource) { diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/BaseJsonHolder.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/BaseJsonHolder.java deleted file mode 100644 index bb859a9c2a..0000000000 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/BaseJsonHolder.java +++ /dev/null @@ -1,46 +0,0 @@ -package im.actor.sdk.controllers.conversation.messages.content; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; - -import im.actor.core.entity.Message; -import im.actor.core.entity.content.JsonContent; -import im.actor.runtime.json.JSONException; -import im.actor.runtime.json.JSONObject; -import im.actor.sdk.R; -import im.actor.sdk.controllers.conversation.messages.MessagesAdapter; -import im.actor.sdk.controllers.conversation.messages.content.preprocessor.PreprocessedData; - -public abstract class BaseJsonHolder extends MessageHolder { - - public BaseJsonHolder(MessagesAdapter adapter, ViewGroup viewGroup, int resourceId, boolean isFullSize) { - super(adapter, inflate(resourceId, viewGroup), isFullSize); - } - - @Override - protected void bindData(Message message, long readDate, long receiveDate, boolean isUpdated, PreprocessedData preprocessedData) { - JSONObject json = null; - JSONObject data = null; - - try { - json = new JSONObject(((JsonContent) message.getContent()).getRawJson()); - - data = json.getJSONObject("data"); - } catch (JSONException e) { - e.printStackTrace(); - } - - bindData(message, data, isUpdated, preprocessedData); - } - - protected abstract void bindData(Message message, JSONObject data, boolean isUpdated, PreprocessedData preprocessedData); - - private static View inflate(int resourceId, ViewGroup viewGroup) { - View base = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.custom_holder, viewGroup, false); - View content = LayoutInflater.from(viewGroup.getContext()).inflate(resourceId, viewGroup, false); - ((FrameLayout) base.findViewById(R.id.custom_container)).addView(content); - return base; - } -} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/ContactHolder.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/ContactHolder.java index 4cecd4f7a9..70059808b3 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/ContactHolder.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/ContactHolder.java @@ -18,6 +18,7 @@ import android.widget.TextView; import im.actor.core.entity.Message; +import im.actor.core.entity.Peer; import im.actor.core.entity.content.ContactContent; import im.actor.sdk.ActorSDK; import im.actor.sdk.R; @@ -47,8 +48,8 @@ public class ContactHolder extends MessageHolder { private ImageView contactAvatar; - public ContactHolder(MessagesAdapter fragment, final View itemView) { - super(fragment, itemView, false); + public ContactHolder(MessagesAdapter adapter, final View itemView, Peer peer) { + super(adapter, itemView, false); waitColor = ActorSDK.sharedActor().style.getConvStatePendingColor(); sentColor = ActorSDK.sharedActor().style.getConvStateSentColor(); deliveredColor = ActorSDK.sharedActor().style.getConvStateDeliveredColor(); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/DocHolder.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/DocHolder.java index e94976ce5a..58dc96925c 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/DocHolder.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/DocHolder.java @@ -13,6 +13,7 @@ import im.actor.core.entity.FileReference; import im.actor.core.entity.Message; +import im.actor.core.entity.Peer; import im.actor.core.entity.content.DocumentContent; import im.actor.core.entity.content.FileLocalSource; import im.actor.core.entity.content.FileRemoteSource; @@ -68,11 +69,11 @@ public class DocHolder extends MessageHolder { protected UploadFileVM uploadFileVM; protected DocumentContent document; - public DocHolder(final MessagesAdapter fragment, View itemView) { - this(fragment, itemView, false); + public DocHolder(final MessagesAdapter adapter, View itemView, Peer peer) { + this(adapter, itemView, false, peer); } - public DocHolder(final MessagesAdapter fragment, View itemView, boolean isFullSize) { + public DocHolder(final MessagesAdapter fragment, View itemView, boolean isFullSize, Peer peer) { super(fragment, itemView, isFullSize); waitColor = ActorSDK.sharedActor().style.getConvStatePendingColor(); sentColor = ActorSDK.sharedActor().style.getConvStateSentColor(); @@ -297,7 +298,7 @@ protected void bindData(Message message, long readDate, long receiveDate, boolea if (document.getSource() instanceof FileRemoteSource) { FileRemoteSource remoteSource = (FileRemoteSource) document.getSource(); - boolean autoDownload = remoteSource.getFileReference().getFileSize() <= 1024 * 1024;// < 1MB + boolean autoDownload = remoteSource.getFileReference().getFileSize() <= 1024 * 1024 && messenger().isDocAutoDownloadEnabled();// < 1MB downloadFileVM = messenger().bindFile(remoteSource.getFileReference(), autoDownload, new DownloadVMCallback()); } else if (document.getSource() instanceof FileLocalSource) { diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/LocationHolder.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/LocationHolder.java index 2d5700f82a..6f23e0662e 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/LocationHolder.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/LocationHolder.java @@ -29,6 +29,7 @@ import java.io.OutputStream; import im.actor.core.entity.Message; +import im.actor.core.entity.Peer; import im.actor.core.entity.content.LocationContent; import im.actor.core.viewmodel.FileVM; import im.actor.core.viewmodel.UploadFileVM; @@ -66,9 +67,9 @@ public class LocationHolder extends MessageHolder { protected UploadFileVM uploadFileVM; protected boolean isPhoto; - public LocationHolder(MessagesAdapter fragment, View itemView) { - super(fragment, itemView, false); - this.context = fragment.getMessagesFragment().getActivity(); + public LocationHolder(MessagesAdapter adapter, View itemView, Peer peer) { + super(adapter, itemView, false); + this.context = adapter.getMessagesFragment().getActivity(); COLOR_PENDING = ActorSDK.sharedActor().style.getConvMediaStatePendingColor(); COLOR_SENT = ActorSDK.sharedActor().style.getConvMediaStateSentColor(); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/MessageHolder.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/MessageHolder.java index 47163afe9b..f62ddffdbd 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/MessageHolder.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/MessageHolder.java @@ -12,7 +12,6 @@ import im.actor.sdk.controllers.conversation.view.BubbleContainer; import im.actor.sdk.controllers.conversation.view.ReactionSpan; import im.actor.sdk.util.DateFormatting; -import im.actor.runtime.android.view.BindedViewHolder; import im.actor.core.entity.Message; import im.actor.core.entity.Peer; import im.actor.core.entity.PeerType; @@ -20,7 +19,7 @@ import static im.actor.sdk.util.ActorSDKMessenger.myUid; import static im.actor.sdk.util.ActorSDKMessenger.users; -public abstract class MessageHolder extends BindedViewHolder +public abstract class MessageHolder extends AbsMessageViewHolder implements BubbleContainer.OnAvatarClickListener, BubbleContainer.OnAvatarLongClickListener, View.OnClickListener, View.OnLongClickListener { protected MessagesAdapter adapter; @@ -58,6 +57,7 @@ public Peer getPeer() { return adapter.getMessagesFragment().getPeer(); } + @Override public final void bindData(Message message, Message prev, Message next, long readDate, long receiveDate, PreprocessedData preprocessedData) { boolean isUpdated = currentMessage == null || currentMessage.getRid() != message.getRid(); currentMessage = message; @@ -147,7 +147,7 @@ public boolean onLongClick(View v) { return false; } - + @Override public void unbind() { currentMessage = null; } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/PhotoHolder.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/PhotoHolder.java index e779e351ad..2f0197eb2f 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/PhotoHolder.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/PhotoHolder.java @@ -29,6 +29,7 @@ import im.actor.core.entity.FileReference; import im.actor.core.entity.Message; +import im.actor.core.entity.Peer; import im.actor.core.entity.content.AnimationContent; import im.actor.core.entity.content.DocumentContent; import im.actor.core.entity.content.FileLocalSource; @@ -101,9 +102,9 @@ public class PhotoHolder extends MessageHolder { private final ControllerListener animationController; private Animatable anim; - public PhotoHolder(MessagesAdapter fragment, View itemView) { - super(fragment, itemView, false); - this.context = fragment.getMessagesFragment().getActivity(); + public PhotoHolder(MessagesAdapter adapter, View itemView, Peer peer) { + super(adapter, itemView, false); + this.context = adapter.getMessagesFragment().getActivity(); COLOR_PENDING = ActorSDK.sharedActor().style.getConvMediaStatePendingColor(); COLOR_SENT = ActorSDK.sharedActor().style.getConvMediaStateSentColor(); @@ -287,7 +288,16 @@ protected void bindData(Message message, long readDate, long receiveDate, boolea progressIcon.setVisibility(View.GONE); if (fileMessage.getSource() instanceof FileRemoteSource) { - boolean autoDownload = (fileMessage instanceof PhotoContent) || (fileMessage instanceof AnimationContent); + + boolean autoDownload = false; + if (fileMessage instanceof PhotoContent) { + autoDownload = messenger().isImageAutoDownloadEnabled(); + } else if (fileMessage instanceof AnimationContent) { + autoDownload = messenger().isAnimationAutoPlayEnabled(); + } else if (fileMessage instanceof VideoContent) { + autoDownload = messenger().isVideoAutoDownloadEnabled(); + } + if (!updated) { previewView.setImageURI(null); } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/ServiceHolder.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/ServiceHolder.java index 1dbd25a7fc..de6c09a1b1 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/ServiceHolder.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/ServiceHolder.java @@ -3,6 +3,9 @@ import android.view.View; import android.widget.TextView; +import im.actor.core.entity.GroupType; +import im.actor.core.entity.Peer; +import im.actor.core.entity.PeerType; import im.actor.sdk.ActorSDK; import im.actor.sdk.R; import im.actor.core.entity.Message; @@ -10,14 +13,18 @@ import im.actor.sdk.controllers.conversation.messages.MessagesAdapter; import im.actor.sdk.controllers.conversation.messages.content.preprocessor.PreprocessedData; +import static im.actor.sdk.util.ActorSDKMessenger.groups; import static im.actor.sdk.util.ActorSDKMessenger.messenger; public class ServiceHolder extends MessageHolder { private TextView messageText; + private boolean isChannel; - public ServiceHolder(MessagesAdapter fragment, View itemView) { - super(fragment, itemView, true); + public ServiceHolder(MessagesAdapter adapter, View itemView, Peer peer) { + super(adapter, itemView, true); + + isChannel = peer.getPeerType() == PeerType.GROUP && groups().get(peer.getPeerId()).getGroupType() == GroupType.CHANNEL; messageText = (TextView) itemView.findViewById(R.id.serviceMessage); messageText.setTextColor(ActorSDK.sharedActor().style.getConvDatetextColor()); @@ -27,6 +34,6 @@ public ServiceHolder(MessagesAdapter fragment, View itemView) { @Override protected void bindData(Message message, long readDate, long receiveDate, boolean isUpdated, PreprocessedData preprocessedData) { messageText.setText(messenger().getFormatter().formatFullServiceMessage(message.getSenderId(), - (ServiceContent) message.getContent())); + (ServiceContent) message.getContent(), isChannel)); } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/StickerHolder.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/StickerHolder.java index ebc7fdf26c..d8fa5618d4 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/StickerHolder.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/StickerHolder.java @@ -8,6 +8,7 @@ import im.actor.core.entity.FileReference; import im.actor.core.entity.ImageLocation; import im.actor.core.entity.Message; +import im.actor.core.entity.Peer; import im.actor.core.entity.content.StickerContent; import im.actor.sdk.ActorSDK; import im.actor.sdk.R; @@ -40,10 +41,10 @@ public class StickerHolder extends MessageHolder { // Content Views private StickerView sticker; - public StickerHolder(MessagesAdapter fragment, View itemView) { + public StickerHolder(MessagesAdapter adapter, View itemView, Peer peer) { - super(fragment, itemView, false); - this.context = fragment.getMessagesFragment().getActivity(); + super(adapter, itemView, false); + this.context = adapter.getMessagesFragment().getActivity(); COLOR_PENDING = ActorSDK.sharedActor().style.getConvMediaStatePendingColor(); COLOR_SENT = ActorSDK.sharedActor().style.getConvMediaStateSentColor(); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/TextHolder.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/TextHolder.java index 5a67ce9ded..bb38af7ec4 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/TextHolder.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/TextHolder.java @@ -9,6 +9,7 @@ import android.widget.TextView; import im.actor.core.entity.Message; +import im.actor.core.entity.Peer; import im.actor.sdk.ActorSDK; import im.actor.sdk.R; import im.actor.sdk.controllers.conversation.messages.MessagesAdapter; @@ -33,8 +34,8 @@ public class TextHolder extends MessageHolder { private int readColor; private int errorColor; - public TextHolder(MessagesAdapter fragment, final View itemView) { - super(fragment, itemView, false); + public TextHolder(MessagesAdapter adapter, final View itemView, Peer peer) { + super(adapter, itemView, false); mainContainer = (ViewGroup) itemView.findViewById(R.id.mainContainer); messageBubble = (FrameLayout) itemView.findViewById(R.id.fl_bubble); @@ -71,6 +72,7 @@ protected void bindData(final Message message, long readDate, long receiveDate, } public void bindRawText(CharSequence rawText, long readDate, long receiveDate, Spannable reactions, Message message, boolean isItalic) { + text.setTag(R.id.peer, getPeer()); if (message.getSenderId() == myUid()) { messageBubble.setBackgroundResource(R.drawable.bubble_text_out); } else { diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/UnsupportedHolder.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/UnsupportedHolder.java index dde71c5305..cccfb3e4f9 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/UnsupportedHolder.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/UnsupportedHolder.java @@ -6,6 +6,7 @@ import android.view.View; +import im.actor.core.entity.Peer; import im.actor.sdk.R; import im.actor.core.entity.Message; import im.actor.sdk.controllers.conversation.messages.MessagesAdapter; @@ -15,10 +16,10 @@ public class UnsupportedHolder extends TextHolder { protected String text; - public UnsupportedHolder(MessagesAdapter fragment, View itemView) { - super(fragment, itemView); + public UnsupportedHolder(MessagesAdapter fragmeadaptert, View itemView, Peer peer) { + super(fragmeadaptert, itemView, peer); - text = fragment.getMessagesFragment().getResources().getString(R.string.chat_unsupported); + text = fragmeadaptert.getMessagesFragment().getResources().getString(R.string.chat_unsupported); onConfigureViewHolder(); } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/preprocessor/ChatListProcessor.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/preprocessor/ChatListProcessor.java index eda0f91a18..ee473ccdb9 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/preprocessor/ChatListProcessor.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/messages/content/preprocessor/ChatListProcessor.java @@ -68,15 +68,7 @@ public ChatListProcessor(Peer peer, Context context) { if (isGroup) { group = groups().get(peer.getPeerId()); } - colors = new int[]{ - context.getResources().getColor(R.color.placeholder_0), - context.getResources().getColor(R.color.placeholder_1), - context.getResources().getColor(R.color.placeholder_2), - context.getResources().getColor(R.color.placeholder_3), - context.getResources().getColor(R.color.placeholder_4), - context.getResources().getColor(R.color.placeholder_5), - context.getResources().getColor(R.color.placeholder_6), - }; + colors = ActorSDK.sharedActor().style.getDefaultAvatarPlaceholders(); } @Nullable diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/toolbar/ChatToolbarFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/toolbar/ChatToolbarFragment.java index 0f51eda4fb..51f717b745 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/toolbar/ChatToolbarFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/toolbar/ChatToolbarFragment.java @@ -4,6 +4,7 @@ import android.app.Activity; import android.app.AlertDialog; import android.content.pm.PackageManager; +import android.graphics.Color; import android.graphics.PorterDuff; import android.os.Bundle; import android.support.v4.app.ActivityCompat; @@ -19,6 +20,7 @@ import android.widget.TextView; import android.widget.Toast; +import im.actor.core.entity.GroupType; import im.actor.core.entity.Peer; import im.actor.core.entity.PeerType; import im.actor.core.viewmodel.Command; @@ -39,38 +41,39 @@ import static im.actor.sdk.util.ActorSDKMessenger.groups; import static im.actor.sdk.util.ActorSDKMessenger.messenger; +import static im.actor.sdk.util.ActorSDKMessenger.myUid; import static im.actor.sdk.util.ActorSDKMessenger.users; public class ChatToolbarFragment extends BaseFragment { public static final int MAX_USERS_FOR_CALLS = 5; - private static final int PERMISSIONS_REQUEST_FOR_CALL = 8; - private static final int PERMISSIONS_REQUEST_FOR_VIDEO_CALL = 12; + protected static final int PERMISSIONS_REQUEST_FOR_CALL = 8; + protected static final int PERMISSIONS_REQUEST_FOR_VIDEO_CALL = 12; public static ChatToolbarFragment create(Peer peer) { return new ChatToolbarFragment(peer); } - private Peer peer; + protected Peer peer; // Toolbar title root view - private View barView; + protected View barView; // Toolbar unread counter - private TextView counter; + protected TextView counter; // Toolbar Avatar view - private AvatarView barAvatar; + protected AvatarView barAvatar; // Toolbar title view - private TextView barTitle; + protected TextView barTitle; // Toolbar subtitle view container - private View barSubtitleContainer; + protected View barSubtitleContainer; // Toolbar subtitle text view - private TextView barSubtitle; + protected TextView barSubtitle; // Toolbar typing container - private View barTypingContainer; + protected View barTypingContainer; // Toolbar typing icon - private ImageView barTypingIcon; + protected ImageView barTypingIcon; // Toolbar typing text - private TextView barTyping; + protected TextView barTyping; public ChatToolbarFragment() { setRootFragment(true); @@ -144,24 +147,18 @@ public void onConfigureActionBar(ActionBar actionBar) { public void onResume() { super.onResume(); - // Performing all required Data Binding here if (peer.getPeerType() == PeerType.PRIVATE) { // Loading user - final UserVM user = users().get(peer.getPeerId()); - if (user == null) { - - return; - } + UserVM user = users().get(peer.getPeerId()); // Binding User Avatar to Toolbar bind(barAvatar, user.getId(), user.getAvatar(), user.getName()); // Binding User name to Toolbar bind(barTitle, user.getName()); - bind(user.getIsVerified(), (val, valueModel) -> { barTitle.setCompoundDrawablesWithIntrinsicBounds(null, null, val ? new TintDrawable( @@ -185,16 +182,19 @@ public void onResume() { // Loading group GroupVM group = groups().get(peer.getPeerId()); - if (group == null) { - // finish(); - return; - } // Binding Group avatar to Toolbar bind(barAvatar, group.getId(), group.getAvatar(), group.getName()); // Binding Group title to Toolbar bind(barTitle, group.getName()); + if (group.getGroupType() == GroupType.CHANNEL) { + barTitle.setCompoundDrawablesWithIntrinsicBounds(new TintDrawable( + getResources().getDrawable(R.drawable.ic_megaphone_18dp_black), + Color.WHITE), null, null, null); + } else { + barTitle.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); + } // Subtitle is always visible for Groups barSubtitleContainer.setVisibility(View.VISIBLE); @@ -203,14 +203,11 @@ public void onResume() { bind(barSubtitle, barSubtitleContainer, group); // Binding group typing - bindGroupTyping(barTyping, barTypingContainer, barSubtitle, messenger().getGroupTyping(group.getId())); - } - - // Show/Hide Avatar - if (!style.isShowAvatarInTitle() || (peer.getPeerType() == PeerType.PRIVATE && !style.isShowAvatarPrivateInTitle())) { - barAvatar.setVisibility(View.GONE); + if (group.getGroupType() == GroupType.GROUP) { + bindGroupTyping(barTyping, barTypingContainer, barSubtitle, messenger().getGroupTyping(group.getId())); + } } - + // Global Counter bind(messenger().getGlobalState().getGlobalCounter(), (val, valueModel) -> { if (val != null && val > 0) { @@ -227,28 +224,35 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); // Inflating menu + menu.clear(); inflater.inflate(R.menu.chat_menu, menu); // Show menu for opening chat contact - if (peer.getPeerType() == PeerType.PRIVATE) { - menu.findItem(R.id.contact).setVisible(true); - } else { - menu.findItem(R.id.contact).setVisible(false); - } +// if (peer.getPeerType() == PeerType.PRIVATE) { +// menu.findItem(R.id.contact).setVisible(true); +// } else { +// menu.findItem(R.id.contact).setVisible(false); +// } // Show menus for leave group and group info view - if (peer.getPeerType() == PeerType.GROUP) { - if (groups().get(peer.getPeerId()).isMember().get()) { - menu.findItem(R.id.leaveGroup).setVisible(true); - menu.findItem(R.id.groupInfo).setVisible(true); - } else { - menu.findItem(R.id.leaveGroup).setVisible(false); - menu.findItem(R.id.groupInfo).setVisible(false); - } - } else { - menu.findItem(R.id.groupInfo).setVisible(false); - menu.findItem(R.id.leaveGroup).setVisible(false); - } +// if (peer.getPeerType() == PeerType.GROUP) { +// GroupVM groupVM = groups().get(peer.getPeerId()); +// if (groupVM.isMember().get()) { +// menu.findItem(R.id.leaveGroup).setVisible(true); +// menu.findItem(R.id.groupInfo).setVisible(true); +// } else { +// menu.findItem(R.id.leaveGroup).setVisible(false); +// menu.findItem(R.id.groupInfo).setVisible(false); +// } +// if (groupVM.getGroupType() == GroupType.GROUP) { +// menu.findItem(R.id.clear).setVisible(true); +// } else { +// menu.findItem(R.id.clear).setVisible(false); +// } +// } else { +// menu.findItem(R.id.groupInfo).setVisible(false); +// menu.findItem(R.id.leaveGroup).setVisible(false); +// } // Voice and Video calls boolean callsEnabled = ActorSDK.sharedActor().isCallsEnabled(); @@ -257,8 +261,15 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { if (peer.getPeerType() == PeerType.PRIVATE) { callsEnabled = !users().get(peer.getPeerId()).isBot(); } else if (peer.getPeerType() == PeerType.GROUP) { - callsEnabled = groups().get(peer.getPeerId()).getMembersCount() <= MAX_USERS_FOR_CALLS; - videoCallsEnabled = false; + + GroupVM groupVM = groups().get(peer.getPeerId()); + if (groupVM.getGroupType() == GroupType.GROUP && groupVM.isMember().get() && groupVM.getIsCanCall().get()) { + callsEnabled = groupVM.getMembersCount().get() <= MAX_USERS_FOR_CALLS; + videoCallsEnabled = false; + } else { + callsEnabled = false; + videoCallsEnabled = false; + } } } menu.findItem(R.id.call).setVisible(callsEnabled); @@ -275,7 +286,7 @@ public boolean onOptionsItemSelected(MenuItem item) { int i = item.getItemId(); if (i == android.R.id.home) { getActivity().finish(); - } else if (i == R.id.clear) { + } /*else if (i == R.id.clear) { new AlertDialog.Builder(getActivity()) .setMessage(R.string.alert_delete_all_messages_text) .setPositiveButton(R.string.alert_delete_all_messages_yes, (dialog, which) -> { @@ -322,7 +333,7 @@ public void onError(final Exception e) { ActorSDKLauncher.startProfileActivity(getActivity(), peer.getPeerId()); } else if (i == R.id.groupInfo) { ActorSDK.sharedActor().startGroupInfoActivity(getActivity(), peer.getPeerId()); - } else if (i == R.id.add_to_contacts) { + }*/ else if (i == R.id.add_to_contacts) { execute(messenger().addContact(peer.getPeerId())); } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/view/MentionSpan.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/view/MentionSpan.java index 99a1ed22d9..938490da37 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/view/MentionSpan.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/conversation/view/MentionSpan.java @@ -21,15 +21,7 @@ public class MentionSpan extends BaseUrlSpan { public MentionSpan(String nick, int userId, boolean hideUrlStyle) { super(nick, hideUrlStyle); this.userId = userId; - colors = new int[]{ - AndroidContext.getContext().getResources().getColor(R.color.placeholder_0), - AndroidContext.getContext().getResources().getColor(R.color.placeholder_1), - AndroidContext.getContext().getResources().getColor(R.color.placeholder_2), - AndroidContext.getContext().getResources().getColor(R.color.placeholder_3), - AndroidContext.getContext().getResources().getColor(R.color.placeholder_4), - AndroidContext.getContext().getResources().getColor(R.color.placeholder_5), - AndroidContext.getContext().getResources().getColor(R.color.placeholder_6), - }; + colors = ActorSDK.sharedActor().style.getDefaultAvatarPlaceholders(); } @Override diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/dialogs/BaseDialogFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/dialogs/BaseDialogFragment.java index 95ad33e059..d43ceb60e3 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/dialogs/BaseDialogFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/dialogs/BaseDialogFragment.java @@ -59,7 +59,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa // Header View header = new View(getActivity()); - header.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ActorSDK.sharedActor().style.getDialogsPaddingTop())); + header.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(ActorSDK.sharedActor().style.getDialogsPaddingTopDp()))); header.setBackgroundColor(ActorSDK.sharedActor().style.getMainBackgroundColor()); addHeaderView(header); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/dialogs/DialogsDefaultFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/dialogs/DialogsDefaultFragment.java index b64b9b6bdf..05efb48533 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/dialogs/DialogsDefaultFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/dialogs/DialogsDefaultFragment.java @@ -5,6 +5,7 @@ import android.widget.Toast; import im.actor.core.entity.Dialog; +import im.actor.core.entity.GroupType; import im.actor.core.entity.PeerType; import im.actor.core.viewmodel.CommandCallback; import im.actor.core.viewmodel.GroupVM; @@ -69,59 +70,56 @@ public void onError(Exception e) { return true; } else if (dialog.getPeer().getPeerType() == PeerType.GROUP) { GroupVM groupVM = groups().get(dialog.getPeer().getPeerId()); - final boolean isMember = groupVM.isMember().get(); - + CharSequence[] items; + int dialogs_menu_view = groupVM.getGroupType() == GroupType.CHANNEL ? R.string.dialogs_menu_channel_view : R.string.dialogs_menu_group_view; + int dialogs_menu_rename = groupVM.getGroupType() == GroupType.CHANNEL ? R.string.dialogs_menu_channel_rename : R.string.dialogs_menu_group_rename; + int dialogs_menu_leave = groupVM.getGroupType() == GroupType.CHANNEL ? R.string.dialogs_menu_channel_leave : R.string.dialogs_menu_group_leave; + int dialogs_menu_delete = groupVM.getGroupType() == GroupType.CHANNEL ? R.string.dialogs_menu_channel_delete : R.string.dialogs_menu_group_delete; + items = new CharSequence[]{ + getString(dialogs_menu_view), + getString(dialogs_menu_rename), + getString(groupVM.getIsCanLeave().get() ? dialogs_menu_leave : + groupVM.getIsCanDelete().get() ? dialogs_menu_delete : + dialogs_menu_leave), + }; new AlertDialog.Builder(getActivity()) - .setItems(new CharSequence[]{ - getString(R.string.dialogs_menu_group_view), - getString(R.string.dialogs_menu_group_rename), - isMember ? getString(R.string.dialogs_menu_group_leave) - : getString(R.string.dialogs_menu_group_delete), - }, (d, which) -> { + .setItems(items, (d, which) -> { if (which == 0) { ActorSDK.sharedActor().startGroupInfoActivity(getActivity(), dialog.getPeer().getPeerId()); } else if (which == 1) { startActivity(Intents.editGroupTitle(dialog.getPeer().getPeerId(), getActivity())); } else if (which == 2) { - if (isMember) { - new AlertDialog.Builder(getActivity()) - .setMessage(getString(R.string.alert_leave_group_message, dialog.getDialogTitle())) - .setNegativeButton(R.string.dialog_cancel, null) - .setPositiveButton(R.string.alert_leave_group_yes, (d1, which1) -> { - execute(messenger().leaveGroup(dialog.getPeer().getPeerId()), R.string.progress_common, - new CommandCallback() { - @Override - public void onResult(Void res) { - - } - - @Override - public void onError(Exception e) { - Toast.makeText(getActivity(), R.string.toast_unable_leave, Toast.LENGTH_LONG).show(); - } - }); - }) - .show(); - } else { - new AlertDialog.Builder(getActivity()) - .setMessage(getString(R.string.alert_delete_group_title, groupVM.getName().get())) - .setNegativeButton(R.string.dialog_cancel, null) - .setPositiveButton(R.string.alert_delete_group_yes, (d1, which1) -> { - execute(messenger().deleteChat(dialog.getPeer()), R.string.progress_common, - new CommandCallback() { - @Override - public void onResult(Void res) { + int alert_delete_title = groupVM.getGroupType() == GroupType.CHANNEL ? R.string.alert_delete_channel_title : R.string.alert_delete_group_title; + int alert_leave_message = groupVM.getGroupType() == GroupType.CHANNEL ? R.string.alert_leave_channel_message : R.string.alert_leave_group_message; + new AlertDialog.Builder(getActivity()) + .setMessage(getString(groupVM.getIsCanLeave().get() ? alert_leave_message : + groupVM.getIsCanDelete().get() ? alert_delete_title : + alert_leave_message, dialog.getDialogTitle())) + .setNegativeButton(R.string.dialog_cancel, null) + .setPositiveButton(groupVM.getIsCanLeave().get() ? R.string.alert_leave_group_yes : R.string.alert_delete_group_yes, (d1, which1) -> { + if (groupVM.getIsCanLeave().get()) { + execute(messenger().leaveAndDeleteGroup(dialog.getPeer().getPeerId()), R.string.progress_common).failure(e -> { + Toast.makeText(getActivity(), R.string.toast_unable_leave, Toast.LENGTH_LONG).show(); + }); + } else if (groupVM.getIsCanDelete().get()) { + execute(messenger().deleteGroup(dialog.getPeer().getPeerId()), R.string.progress_common).failure(e -> { + Toast.makeText(getActivity(), R.string.toast_unable_delete_chat, Toast.LENGTH_LONG).show(); + }); + } else { + execute(messenger().deleteChat(dialog.getPeer()), R.string.progress_common, new CommandCallback() { + @Override + public void onResult(Void res) { - } + } - @Override - public void onError(Exception e) { - Toast.makeText(getActivity(), R.string.toast_unable_delete_chat, Toast.LENGTH_LONG).show(); - } - }); - }) - .show(); - } + @Override + public void onError(Exception e) { + Toast.makeText(getActivity(), R.string.toast_unable_delete_chat, Toast.LENGTH_LONG).show(); + } + }); + } + }) + .show(); } }).show(); return true; diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/dialogs/view/DialogView.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/dialogs/view/DialogView.java index f3330dd89c..146cc812a2 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/dialogs/view/DialogView.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/dialogs/view/DialogView.java @@ -53,6 +53,7 @@ public class DialogView extends ListItemBackgroundView 0) { + String contentText = messenger().getFormatter().formatContentText(arg.getSenderId(), - arg.getMessageType(), arg.getText().replace("\n", " "), arg.getRelatedUid()); + arg.getMessageType(), arg.getText().replace("\n", " "), arg.getRelatedUid(), + arg.isChannel()); if (arg.getPeer().getPeerType() == PeerType.GROUP) { if (messenger().getFormatter().isLargeDialogMessage(arg.getMessageType())) { @@ -516,6 +542,7 @@ public static class DialogLayout { private CharSequence shortName; private Layout titleLayout; private Drawable titleIcon; + private int titleIconTop; private String date; private int dateWidth; private Layout textLayout; @@ -523,6 +550,14 @@ public static class DialogLayout { private int counterWidth; private Drawable state; + public int getTitleIconTop() { + return titleIconTop; + } + + public void setTitleIconTop(int titleIconTop) { + this.titleIconTop = titleIconTop; + } + public Drawable getState() { return state; } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/fragment/help/HelpActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/fragment/help/HelpActivity.java index efbbcb3fa2..f931aea58f 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/fragment/help/HelpActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/fragment/help/HelpActivity.java @@ -12,6 +12,6 @@ protected void onCreate(Bundle savedInstanceState) { getSupportActionBar().setTitle(R.string.help_title); - showFragment(new HelpFragment(), false, false); + showFragment(new HelpFragment(), false); } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/fragment/preview/PictureActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/fragment/preview/PictureActivity.java index 1224218f15..b94cd6a74a 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/fragment/preview/PictureActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/fragment/preview/PictureActivity.java @@ -1,17 +1,20 @@ package im.actor.sdk.controllers.fragment.preview; +import android.Manifest; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.app.Activity; import android.content.Intent; +import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.media.MediaScannerConnection; -import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; -import android.provider.MediaStore; +import android.support.annotation.NonNull; +import android.support.v13.app.ActivityCompat; import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.view.LayoutInflater; @@ -30,6 +33,9 @@ import java.io.File; import java.io.IOException; +import im.actor.core.entity.FileReference; +import im.actor.core.viewmodel.UserVM; +import im.actor.runtime.Log; import im.actor.sdk.ActorSDK; import im.actor.sdk.R; import im.actor.sdk.controllers.Intents; @@ -39,10 +45,8 @@ import im.actor.sdk.util.Screen; import im.actor.sdk.util.images.common.ImageLoadException; import im.actor.sdk.util.images.ops.ImageLoading; -import im.actor.sdk.view.avatar.AvatarView; import im.actor.sdk.view.MaterialInterpolator; -import im.actor.core.entity.FileReference; -import im.actor.core.viewmodel.UserVM; +import im.actor.sdk.view.avatar.AvatarView; import uk.co.senab.photoview.DefaultOnDoubleTapListener; import uk.co.senab.photoview.PhotoViewAttacher; @@ -50,6 +54,7 @@ public class PictureActivity extends BaseActivity { + private static final int PERMISSION_REQ_MEDIA = 0; private static final String ARG_FILE_SIZE = "ARG_FILE_SIZE"; private static final String ARG_FILE_ACCESS_HASH = "ARG_FILE_ACCESS"; @@ -250,6 +255,7 @@ public static class PictureFragment extends Fragment { private String fileName; private CircularView circularView; private View backgroundView; + private MenuItem saveMenuItem; public PictureFragment() { } @@ -463,6 +469,7 @@ public void onDestroyView() { @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.media_picture, menu); + saveMenuItem = menu.findItem(R.id.save); } @Override @@ -474,30 +481,50 @@ public boolean onOptionsItemSelected(MenuItem item) { .putExtra(Intent.EXTRA_STREAM,Uri.parse(path)));*/ return true; } else if (item.getItemId() == R.id.save) { - File externalFile = Environment.getExternalStorageDirectory(); - if (externalFile == null) { - Toast.makeText(getActivity(), R.string.toast_no_sdcard, Toast.LENGTH_LONG).show(); - }else{ - boolean isGif = path.endsWith(".gif"); - String externalPath = externalFile.getAbsolutePath(); - String exportPathBase = externalPath + "/" + ActorSDK.sharedActor().getAppName() + "/" + ActorSDK.sharedActor().getAppName() + " images" + "/"; - new File(exportPathBase).mkdirs(); - try { - String exportPath = exportPathBase + (fileName != null ? fileName : "exported") + "_" + Randoms.randomId() + (isGif ? ".gif" : ".jpg"); - Files.copy(new File(this.path), new File(exportPath)); - MediaScannerConnection.scanFile(getActivity(), new String[]{exportPath}, new String[]{"image/" + (isGif ? "gif" : "jpeg")}, null); - Toast.makeText(getActivity(), getString(R.string.file_saved)+ " " + exportPath, Toast.LENGTH_LONG).show(); - item.setEnabled(false); - item.setTitle(R.string.menu_saved); - } catch (IOException e) { - e.printStackTrace(); - } + savePicture(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void savePicture() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(getActivity(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_REQ_MEDIA); + Log.d("Permissions", "savePhoto - no permission :c"); + return; } + } + File externalFile = Environment.getExternalStorageDirectory(); + if (externalFile == null) { + Toast.makeText(getActivity(), R.string.toast_no_sdcard, Toast.LENGTH_LONG).show(); + } else { + boolean isGif = path.endsWith(".gif"); + String externalPath = externalFile.getAbsolutePath(); + String exportPathBase = externalPath + "/" + ActorSDK.sharedActor().getAppName() + "/" + ActorSDK.sharedActor().getAppName() + " images" + "/"; + new File(exportPathBase).mkdirs(); + try { + String exportPath = exportPathBase + (fileName != null ? fileName : "exported") + "_" + Randoms.randomId() + (isGif ? ".gif" : ".jpg"); + Files.copy(new File(this.path), new File(exportPath)); + MediaScannerConnection.scanFile(getActivity(), new String[]{exportPath}, new String[]{"image/" + (isGif ? "gif" : "jpeg")}, null); + Toast.makeText(getActivity(), getString(R.string.file_saved) + " " + exportPath, Toast.LENGTH_LONG).show(); + saveMenuItem.setEnabled(false); + saveMenuItem.setTitle(R.string.menu_saved); + } catch (IOException e) { + e.printStackTrace(); + } + } + } - return true; + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == PERMISSION_REQ_MEDIA) { + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + savePicture(); + } } - return super.onOptionsItemSelected(item); } private void showSystemUi() { @@ -531,7 +558,6 @@ private void syncUiState() { ownerContainer.clearAnimation(); if (uiIsHidden) { - toolbar.animate() .setInterpolator(new MaterialInterpolator()) .y(-toolbar.getHeight()) @@ -608,7 +634,6 @@ public static Fragment getInstance(long fileId, int senderId) { return fragment; } - public static Fragment getInstance(FileReference ref, int senderId) { Bundle bundle = new Bundle(); bundle.putLong(ARG_FILE_ID, ref.getFileId()); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/fragment/preview/ViewAvatarActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/fragment/preview/ViewAvatarActivity.java index 94a0ecc851..2246b09200 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/fragment/preview/ViewAvatarActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/fragment/preview/ViewAvatarActivity.java @@ -252,9 +252,7 @@ public void onDownloaded(FileSystemReference reference) { public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.avatar, menu); - if (peer.getPeerType() == PeerType.GROUP) { - menu.findItem(R.id.editAvatar).setVisible(true); - } else if (peer.getPeerType() == PeerType.PRIVATE && peer.getPeerId() == myUid()) { + if (peer.getPeerType() == PeerType.PRIVATE && peer.getPeerId() == myUid()) { menu.findItem(R.id.editAvatar).setVisible(true); } else { menu.findItem(R.id.editAvatar).setVisible(false); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/AddMemberActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/AddMemberActivity.java index cad1ea9d69..2418ac7f1a 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/AddMemberActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/AddMemberActivity.java @@ -2,23 +2,17 @@ import android.os.Bundle; -import im.actor.sdk.R; +import im.actor.sdk.controllers.Intents; import im.actor.sdk.controllers.activity.BaseFragmentActivity; public class AddMemberActivity extends BaseFragmentActivity { - private int gid; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - getSupportActionBar().setTitle(R.string.group_add_title); - - gid = getIntent().getIntExtra("GROUP_ID", 0); - if (savedInstanceState == null) { - showFragment(AddMemberFragment.create(gid), false, false); + showFragment(AddMemberFragment.create(getIntent().getIntExtra(Intents.EXTRA_GROUP_ID, 0)), false); } } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/AddMemberFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/AddMemberFragment.java index 850264d732..5ca6c706bc 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/AddMemberFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/AddMemberFragment.java @@ -16,16 +16,11 @@ import im.actor.sdk.controllers.Intents; import im.actor.sdk.controllers.contacts.BaseContactFragment; -import static im.actor.sdk.util.ActorSDKMessenger.groups; import static im.actor.sdk.util.ActorSDKMessenger.messenger; import static im.actor.sdk.util.ActorSDKMessenger.users; public class AddMemberFragment extends BaseContactFragment { - public AddMemberFragment() { - super(true, true, false); - } - public static AddMemberFragment create(int gid) { AddMemberFragment res = new AddMemberFragment(); Bundle arguments = new Bundle(); @@ -34,22 +29,39 @@ public static AddMemberFragment create(int gid) { return res; } + private GroupVM groupVM; + + public AddMemberFragment() { + super(true, true, false); + setRootFragment(true); + setTitle(R.string.group_add_title); + setHomeAsUp(true); + } + + @Override + public void onCreate(Bundle saveInstance) { + super.onCreate(saveInstance); + + groupVM = messenger().getGroup(getArguments().getInt("GROUP_ID")); + } + @Override protected void addFootersAndHeaders() { - addFooterOrHeaderAction(ActorSDK.sharedActor().style.getActionAddContactColor(), R.drawable.ic_person_add_white_24dp, R.string.contacts_invite_via_link, false, new Runnable() { - @Override - public void run() { - startActivity(Intents.inviteLink(getArguments().getInt("GROUP_ID", 0), getActivity())); - } - }, true); + + if (groupVM.getIsCanInviteViaLink().get()) { + addFooterOrHeaderAction(ActorSDK.sharedActor().style.getActionAddContactColor(), R.drawable.ic_person_add_white_24dp, R.string.contacts_invite_via_link, false, new Runnable() { + @Override + public void run() { + startActivity(Intents.inviteLink(getArguments().getInt("GROUP_ID", 0), getActivity())); + } + }, true); + } } @Override public void onItemClicked(Contact contact) { - final int gid = getArguments().getInt("GROUP_ID"); final UserVM userModel = users().get(contact.getUid()); - final GroupVM groupVM = groups().get(gid); for (GroupMember uid : groupVM.getMembers().get()) { if (uid.getUid() == userModel.getId()) { @@ -63,7 +75,7 @@ public void onItemClicked(Contact contact) { .setPositiveButton(R.string.alert_group_add_yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog2, int which) { - execute(messenger().inviteMember(gid, userModel.getId()), + execute(messenger().inviteMember(groupVM.getId(), userModel.getId()), R.string.progress_common, new CommandCallback() { @Override public void onResult(Void res) { diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupAdminActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupAdminActivity.java new file mode 100644 index 0000000000..3aab5e0492 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupAdminActivity.java @@ -0,0 +1,19 @@ +package im.actor.sdk.controllers.group; + +import android.os.Bundle; + +import im.actor.sdk.controllers.Intents; +import im.actor.sdk.controllers.activity.BaseFragmentActivity; + +public class GroupAdminActivity extends BaseFragmentActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState == null) { + showFragment(GroupAdminFragment.create( + getIntent().getIntExtra(Intents.EXTRA_GROUP_ID, 0)), false); + } + } +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupAdminFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupAdminFragment.java new file mode 100644 index 0000000000..527d8e51a5 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupAdminFragment.java @@ -0,0 +1,189 @@ +package im.actor.sdk.controllers.group; + +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import im.actor.core.entity.GroupType; +import im.actor.core.viewmodel.GroupVM; +import im.actor.runtime.mvvm.Value; +import im.actor.runtime.mvvm.ValueChangedListener; +import im.actor.sdk.ActorSDK; +import im.actor.sdk.R; +import im.actor.sdk.controllers.BaseFragment; +import im.actor.sdk.controllers.Intents; + +import static im.actor.sdk.util.ActorSDKMessenger.messenger; + +public class GroupAdminFragment extends BaseFragment { + + public static GroupAdminFragment create(int groupId) { + Bundle bundle = new Bundle(); + bundle.putInt("groupId", groupId); + GroupAdminFragment editFragment = new GroupAdminFragment(); + editFragment.setArguments(bundle); + return editFragment; + } + + private GroupVM groupVM; + + public GroupAdminFragment() { + setRootFragment(true); + setHomeAsUp(true); + setShowHome(true); + } + + @Override + public void onCreate(Bundle saveInstance) { + super.onCreate(saveInstance); + groupVM = messenger().getGroup(getArguments().getInt("groupId")); + setTitle(groupVM.getGroupType() == GroupType.CHANNEL ? R.string.channel_admin_title : R.string.group_admin_title); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View res = inflater.inflate(R.layout.fragment_edit_admin, container, false); + res.findViewById(R.id.rootContainer).setBackgroundColor(style.getBackyardBackgroundColor()); + + // Group Type + TextView groupTypeTitle = (TextView) res.findViewById(R.id.groupTypeTitle); + TextView groupTypeValue = (TextView) res.findViewById(R.id.groupTypeValue); + groupTypeTitle.setTextColor(style.getTextPrimaryColor()); + groupTypeValue.setTextColor(style.getListActionColor()); + if (groupVM.getGroupType() == GroupType.CHANNEL) { + groupTypeTitle.setText(R.string.channel_type); + } else { + groupTypeTitle.setText(R.string.group_type); + } + + bind(groupVM.getShortName(), new ValueChangedListener() { + @Override + public void onChanged(String val, Value valueModel) { + if (val == null) { + groupTypeValue.setText(groupVM.getGroupType() == GroupType.CHANNEL ? R.string.channel_type_private : R.string.group_type_private); + } else { + groupTypeValue.setText(groupVM.getGroupType() == GroupType.CHANNEL ? R.string.channel_type_pubic : R.string.group_type_pubic); + } + } + }); + + if (groupVM.getIsCanEditAdministration().get()) { + res.findViewById(R.id.groupTypeContainer).setOnClickListener(v -> { + startActivity(new Intent(getContext(), GroupTypeActivity.class) + .putExtra(Intents.EXTRA_GROUP_ID, groupVM.getId())); + }); + } + + // Share History + View shareContainer = res.findViewById(R.id.shareHistoryContainer); + TextView shareHint = (TextView) res.findViewById(R.id.shareHistoryHint); + shareHint.setTextColor(style.getTextSecondaryColor()); + TextView shareHistory = (TextView) res.findViewById(R.id.shareHistory); + shareHistory.setTextColor(style.getTextPrimaryColor()); + TextView shareHistoryValue = (TextView) res.findViewById(R.id.shareHistoryValue); + shareHistoryValue.setTextColor(style.getListActionColor()); + if (groupVM.getGroupType() == GroupType.GROUP && + groupVM.getIsCanEditAdministration().get()) { + bind(groupVM.getIsHistoryShared(), isShared -> { + if (isShared) { + shareHistoryValue.setVisibility(View.VISIBLE); + shareHistory.setOnClickListener(null); + shareHistory.setClickable(false); + } else { + shareHistoryValue.setVisibility(View.GONE); + shareHistory.setOnClickListener(v -> { + execute(messenger().shareHistory(groupVM.getId())); + }); + } + }); + } else { + // Hide for channels + shareContainer.setVisibility(View.GONE); + shareHint.setVisibility(View.GONE); + } + + // Permissions + TextView permissions = (TextView) res.findViewById(R.id.permissions); + permissions.setTextColor(style.getTextPrimaryColor()); + TextView permissionsHint = (TextView) res.findViewById(R.id.permissionsHint); + permissionsHint.setTextColor(style.getTextSecondaryColor()); + View permissionsDiv = res.findViewById(R.id.permissionsDiv); + if (groupVM.getGroupType() == GroupType.CHANNEL) { + permissionsHint.setText(R.string.channel_permissions_hint); + } else { + permissionsHint.setText(R.string.group_permissions_hint); + } + if (groupVM.getIsCanEditAdministration().get() && groupVM.getGroupType() == GroupType.GROUP) { + permissions.setOnClickListener(v -> { + startActivity(new Intent(getContext(), GroupPermissionsActivity.class) + .putExtra(Intents.EXTRA_GROUP_ID, groupVM.getId())); + }); + } else { + permissions.setVisibility(View.GONE); + permissionsDiv.setVisibility(View.GONE); + permissionsHint.setVisibility(View.GONE); + } + + // Group Deletion + View deleteContainer = res.findViewById(R.id.deleteContainer); + TextView delete = (TextView) res.findViewById(R.id.delete); + delete.setTextColor(style.getTextDangerColor()); + TextView deleteHint = (TextView) res.findViewById(R.id.deleteHint); + deleteHint.setTextColor(style.getTextSecondaryColor()); + + if (groupVM.getGroupType() == GroupType.CHANNEL) { + delete.setText(R.string.channel_delete); + deleteHint.setText(R.string.channel_delete_hint); + } else { + delete.setText(R.string.group_delete); + deleteHint.setText(R.string.group_delete_hint); + } + + bind(groupVM.getIsCanLeave(), groupVM.getIsCanDelete(), (canLeave, canDelete) -> { + if (canLeave || canDelete) { + deleteContainer.setVisibility(View.VISIBLE); + delete.setOnClickListener(v -> { + int alert_delete_title = groupVM.getGroupType() == GroupType.CHANNEL ? R.string.alert_delete_channel_title : R.string.alert_delete_group_title; + int alert_leave_message = groupVM.getGroupType() == GroupType.CHANNEL ? R.string.alert_leave_channel_message : R.string.alert_leave_group_message; + new AlertDialog.Builder(getActivity()) + .setMessage(getString(groupVM.getIsCanLeave().get() ? alert_leave_message : + groupVM.getIsCanDelete().get() ? alert_delete_title : + alert_leave_message, groupVM.getName().get())) + .setNegativeButton(R.string.dialog_cancel, null) + .setPositiveButton(R.string.alert_delete_group_yes, (d1, which1) -> { + if (groupVM.getIsCanLeave().get()) { + execute(messenger().leaveAndDeleteGroup(groupVM.getId()), R.string.progress_common) + .then(aVoid -> ActorSDK.returnToRoot(getActivity())) + .failure(e -> { + Toast.makeText(getActivity(), R.string.toast_unable_leave, Toast.LENGTH_LONG).show(); + }); + } else if (groupVM.getIsCanDelete().get()) { + execute(messenger().deleteGroup(groupVM.getId()), R.string.progress_common) + .then(aVoid -> ActorSDK.returnToRoot(getActivity())) + .failure(e -> { + Toast.makeText(getActivity(), R.string.toast_unable_delete_chat, Toast.LENGTH_LONG).show(); + }); + } + }) + .show(); + + }); + + } else { + deleteContainer.setVisibility(View.GONE); + delete.setOnClickListener(null); + delete.setClickable(false); + } + }); + + + return res; + } +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupEditActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupEditActivity.java new file mode 100644 index 0000000000..71b2892ff4 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupEditActivity.java @@ -0,0 +1,19 @@ +package im.actor.sdk.controllers.group; + +import android.os.Bundle; + +import im.actor.sdk.controllers.Intents; +import im.actor.sdk.controllers.activity.BaseFragmentActivity; + +public class GroupEditActivity extends BaseFragmentActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState == null) { + showFragment(GroupEditFragment.create( + getIntent().getIntExtra(Intents.EXTRA_GROUP_ID, 0)), false); + } + } +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupEditFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupEditFragment.java new file mode 100644 index 0000000000..47a9049bb7 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupEditFragment.java @@ -0,0 +1,209 @@ +package im.actor.sdk.controllers.group; + +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.Toast; + +import com.rengwuxian.materialedittext.MaterialEditText; + +import org.jetbrains.annotations.Nullable; + +import im.actor.core.util.JavaUtil; +import im.actor.core.viewmodel.GroupVM; +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.promise.Promise; +import im.actor.sdk.R; +import im.actor.sdk.controllers.BaseFragment; +import im.actor.sdk.controllers.tools.MediaPickerCallback; +import im.actor.sdk.controllers.tools.MediaPickerFragment; +import im.actor.sdk.util.Screen; +import im.actor.sdk.view.avatar.AvatarView; + +import static im.actor.sdk.util.ActorSDKMessenger.messenger; + +public class GroupEditFragment extends BaseFragment implements MediaPickerCallback { + + public static GroupEditFragment create(int groupId) { + Bundle bundle = new Bundle(); + bundle.putInt("groupId", groupId); + GroupEditFragment editFragment = new GroupEditFragment(); + editFragment.setArguments(bundle); + return editFragment; + } + + private int groupId; + private GroupVM groupVM; + private AvatarView avatarView; + private MaterialEditText titleEditText; + private EditText descriptionEditText; + + public GroupEditFragment() { + setTitle(R.string.group_title); + setHomeAsUp(true); + setShowHome(true); + setRootFragment(true); + } + + @Override + public void onCreate(Bundle saveInstance) { + super.onCreate(saveInstance); + groupId = getArguments().getInt("groupId"); + groupVM = messenger().getGroup(groupId); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View res = inflater.inflate(R.layout.fragment_edit_info, container, false); + res.findViewById(R.id.rootContainer).setBackgroundColor(style.getBackyardBackgroundColor()); + res.findViewById(R.id.topContainer).setBackgroundColor(style.getMainBackgroundColor()); + + avatarView = (AvatarView) res.findViewById(R.id.avatar); + avatarView.init(Screen.dp(52), 24); + avatarView.bind(groupVM.getAvatar().get(), groupVM.getName().get(), groupId); + avatarView.setOnClickListener(v -> { + onAvatarClicked(); + }); + + titleEditText = (MaterialEditText) res.findViewById(R.id.name); + titleEditText.setTextColor(style.getTextPrimaryColor()); + titleEditText.setBaseColor(style.getAccentColor()); + titleEditText.setMetHintTextColor(style.getTextHintColor()); + titleEditText.setText(groupVM.getName().get()); + titleEditText.addTextChangedListener(new TextWatcher() { + + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void afterTextChanged(Editable editable) { + avatarView.updatePlaceholder(editable.toString(), groupId); + } + }); + + descriptionEditText = (EditText) res.findViewById(R.id.description); + descriptionEditText.setTextColor(style.getTextPrimaryColor()); + descriptionEditText.setHintTextColor(style.getTextHintColor()); + descriptionEditText.setText(groupVM.getAbout().get()); + + // Media Picker + getChildFragmentManager().beginTransaction() + .add(new MediaPickerFragment(), "picker") + .commitNow(); + + return res; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.next, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.next) { + + Promise res = Promise.success(null); + boolean isPerformed = false; + + if (titleEditText != null) { + String title = titleEditText.getText().toString().trim(); + if (title.length() > 0) { + if (!title.equals(messenger().getGroup(groupId).getName().get())) { + res = res.chain(r -> messenger().editGroupTitle(groupId, title)); + isPerformed = true; + } + } else { + return true; + } + } + + if (descriptionEditText != null) { + String description = descriptionEditText.getText().toString().trim(); + if (description.length() == 0) { + description = null; + } + + if (!JavaUtil.equalsE(description, groupVM.getAbout().get())) { + final String finalDescription = description; + res = res.chain(r -> messenger().editGroupAbout(groupId, finalDescription)); + isPerformed = true; + } + } + + if (isPerformed) { + execute(res).then(r -> { + finishActivity(); + }).failure(r -> { + Toast.makeText(getActivity(), R.string.toast_unable_change, Toast.LENGTH_SHORT).show(); + }); + } else { + finishActivity(); + } + + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + public void onAvatarClicked() { + CharSequence[] args; + if (groupVM.getAvatar().get() != null) { + args = new CharSequence[]{getString(R.string.pick_photo_camera), + getString(R.string.pick_photo_gallery), + getString(R.string.pick_photo_remove)}; + } else { + args = new CharSequence[]{getString(R.string.pick_photo_camera), + getString(R.string.pick_photo_gallery)}; + } + new AlertDialog.Builder(getActivity()).setItems(args, (d, which) -> { + if (which == 0) { + findPicker().requestPhoto(true); + } else if (which == 1) { + findPicker().requestGallery(true); + } else if (which == 2) { + messenger().removeGroupAvatar(groupId); + avatarView.bind(null, groupVM.getName().get(), groupId); + } + }).show(); + } + + @Override + public void onPhotoCropped(String path) { + messenger().changeGroupAvatar(groupId, path); + avatarView.bindRaw(path); + } + + private MediaPickerFragment findPicker() { + return (MediaPickerFragment) getChildFragmentManager().findFragmentByTag("picker"); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + titleEditText = null; + descriptionEditText = null; + if (avatarView != null) { + avatarView.unbind(); + avatarView = null; + } + } +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupInfoActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupInfoActivity.java index 8edc540316..be166a33c4 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupInfoActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupInfoActivity.java @@ -1,34 +1,25 @@ package im.actor.sdk.controllers.group; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; import android.os.Bundle; +import android.support.v4.app.Fragment; import im.actor.sdk.ActorSDK; import im.actor.sdk.controllers.Intents; import im.actor.sdk.controllers.activity.BaseFragmentActivity; -import im.actor.sdk.controllers.settings.BaseGroupInfoActivity; public class GroupInfoActivity extends BaseFragmentActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - int chatId = getIntent().getIntExtra(Intents.EXTRA_GROUP_ID, 0); - - getSupportActionBar().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); - getSupportActionBar().setTitle(null); - + if (savedInstanceState == null) { - GroupInfoFragment fragment; - BaseGroupInfoActivity profileIntent = ActorSDK.sharedActor().getDelegate().getGroupInfoIntent(chatId); - if (profileIntent != null) { - fragment = profileIntent.getGroupInfoFragment(chatId); - } else { - fragment = GroupInfoFragment.create(chatId); + int groupId = getIntent().getIntExtra(Intents.EXTRA_GROUP_ID, 0); + Fragment profileIntent = ActorSDK.sharedActor().getDelegate().fragmentForGroupInfo(groupId); + if (profileIntent == null) { + profileIntent = GroupInfoFragment.create(groupId); } - - showFragment(fragment, false, false); + showFragment(profileIntent, false); } } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupInfoFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupInfoFragment.java index bdfa0fe42b..8cbaf20a9e 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupInfoFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupInfoFragment.java @@ -1,12 +1,14 @@ package im.actor.sdk.controllers.group; -import android.app.Activity; import android.app.AlertDialog; -import android.content.DialogInterface; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; +import android.support.v7.app.ActionBar; import android.support.v7.widget.SwitchCompat; import android.view.LayoutInflater; import android.view.Menu; @@ -15,8 +17,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; -import android.widget.AdapterView; -import android.widget.CompoundButton; import android.widget.TextView; import android.widget.Toast; @@ -25,9 +25,9 @@ import com.google.i18n.phonenumbers.Phonenumber; import java.util.ArrayList; -import java.util.HashSet; import im.actor.core.entity.GroupMember; +import im.actor.core.entity.GroupType; import im.actor.core.entity.Peer; import im.actor.core.viewmodel.CommandCallback; import im.actor.core.viewmodel.GroupVM; @@ -38,19 +38,17 @@ import im.actor.sdk.ActorSDKLauncher; import im.actor.sdk.ActorStyle; import im.actor.sdk.R; +import im.actor.sdk.controllers.ActorBinder; import im.actor.sdk.controllers.Intents; import im.actor.sdk.controllers.activity.BaseActivity; -import im.actor.sdk.controllers.ActorBinder; import im.actor.sdk.controllers.BaseFragment; import im.actor.sdk.controllers.group.view.MembersAdapter; import im.actor.sdk.controllers.fragment.preview.ViewAvatarActivity; +import im.actor.sdk.util.AlertListBuilder; import im.actor.sdk.util.Screen; import im.actor.sdk.view.TintImageView; import im.actor.sdk.view.adapters.RecyclerListView; -import im.actor.sdk.view.avatar.CoverAvatarView; -import im.actor.sdk.util.Fonts; -import im.actor.runtime.mvvm.ValueChangedListener; -import im.actor.runtime.mvvm.Value; +import im.actor.sdk.view.avatar.AvatarView; import static im.actor.sdk.util.ActorSDKMessenger.groups; import static im.actor.sdk.util.ActorSDKMessenger.messenger; @@ -60,6 +58,7 @@ public class GroupInfoFragment extends BaseFragment { private static final String EXTRA_CHAT_ID = "chat_id"; + private ActorBinder.Binding[] memberBindings; public static GroupInfoFragment create(int chatId) { Bundle args = new Bundle(); @@ -69,21 +68,24 @@ public static GroupInfoFragment create(int chatId) { return res; } - private String[] theme; - private String[] about; private int chatId; - private GroupVM groupInfo; + private GroupVM groupVM; + private RecyclerListView listView; + private AvatarView avatarView; private MembersAdapter groupUserAdapter; - private CoverAvatarView avatarView; private View notMemberView; - protected View header; - private boolean isAdmin; + + public GroupInfoFragment() { + setRootFragment(true); + setHomeAsUp(true); + setShowHome(true); + } @Override - public void onCreate(Bundle saveInstance) { - super.onCreate(saveInstance); - setHasOptionsMenu(true); + public void onConfigureActionBar(ActionBar actionBar) { + super.onConfigureActionBar(actionBar); + actionBar.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); } @Override @@ -91,242 +93,238 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa chatId = getArguments().getInt(EXTRA_CHAT_ID); - groupInfo = groups().get(chatId); + groupVM = groups().get(chatId); View res = inflater.inflate(R.layout.fragment_group, container, false); - res.setBackgroundColor(ActorSDK.sharedActor().style.getMainBackgroundColor()); + listView = (RecyclerListView) res.findViewById(R.id.groupList); notMemberView = res.findViewById(R.id.notMember); - notMemberView.setBackgroundColor(ActorSDK.sharedActor().style.getMainBackgroundColor()); - ActorStyle style = ActorSDK.sharedActor().style; + + res.setBackgroundColor(style.getMainBackgroundColor()); + // listView.setBackgroundColor(style.getMainBackgroundColor()); + notMemberView.setBackgroundColor(style.getMainBackgroundColor()); ((TextView) notMemberView.findViewById(R.id.not_member_text)).setTextColor(style.getTextPrimaryColor()); - bind(groupInfo.isMember(), new ValueChangedListener() { - @Override - public void onChanged(Boolean val, Value Value) { - notMemberView.setVisibility(val ? View.GONE : View.VISIBLE); - getActivity().invalidateOptionsMenu(); - } - }); - listView = (RecyclerListView) res.findViewById(R.id.groupList); - listView.setBackgroundColor(style.getMainBackgroundColor()); + // + // Header + // - header = inflater.inflate(R.layout.fragment_group_header, listView, false); - header.setBackgroundColor(style.getMainBackgroundColor()); + // Views + View header = inflater.inflate(R.layout.fragment_group_header, listView, false); + TextView title = (TextView) header.findViewById(R.id.title); + TextView subtitle = (TextView) header.findViewById(R.id.subtitle); + avatarView = (AvatarView) header.findViewById(R.id.avatar); + avatarView.init(Screen.dp(48), 22); - // Avatar - avatarView = (CoverAvatarView) header.findViewById(R.id.avatar); - bind(avatarView, groupInfo.getAvatar()); - avatarView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - startActivity(ViewAvatarActivity.viewGroupAvatar(chatId, getActivity())); - } - }); + TextView aboutTV = (TextView) header.findViewById(R.id.about); + View shortNameCont = header.findViewById(R.id.shortNameContainer); + TextView shortNameView = (TextView) header.findViewById(R.id.shortName); + TextView shortLinkView = (TextView) header.findViewById(R.id.shortNameLink); - // Title - TextView title = (TextView) header.findViewById(R.id.title); + TextView addMember = (TextView) header.findViewById(R.id.addMemberAction); + addMember.setText(groupVM.getGroupType() == GroupType.CHANNEL ? R.string.channel_add_member : R.string.group_add_member); + + TextView members = (TextView) header.findViewById(R.id.viewMembersAction); + TextView leaveAction = (TextView) header.findViewById(R.id.leaveAction); + TextView administrationAction = (TextView) header.findViewById(R.id.administrationAction); + + View descriptionContainer = header.findViewById(R.id.descriptionContainer); + SwitchCompat isNotificationsEnabled = (SwitchCompat) header.findViewById(R.id.enableNotifications); + + // Styling + // ((TextView) header.findViewById(R.id.about_hint)).setTextColor(style.getTextSecondaryColor()); + header.setBackgroundColor(style.getMainBackgroundColor()); + header.findViewById(R.id.avatarContainer).setBackgroundColor(style.getToolBarColor()); title.setTextColor(style.getProfileTitleColor()); - bind(title, groupInfo.getName()); - - // Created by - isAdmin = false; - final TextView createdBy = (TextView) header.findViewById(R.id.createdBy); - createdBy.setTextColor(style.getProfileSubtitleColor()); - if (groupInfo.getCreatorId() == myUid()) { - createdBy.setText(R.string.group_created_by_you); - isAdmin = true; + subtitle.setTextColor(style.getProfileSubtitleColor()); + aboutTV.setTextColor(style.getTextPrimaryColor()); + shortNameView.setTextColor(style.getTextPrimaryColor()); + shortLinkView.setTextColor(style.getTextSecondaryColor()); + // settingsHeaderText.setTextColor(style.getSettingsCategoryTextColor()); + + ((TintImageView) header.findViewById(R.id.settings_notification_icon)) + .setTint(style.getSettingsIconColor()); + ((TintImageView) header.findViewById(R.id.settings_about_icon)) + .setTint(style.getSettingsIconColor()); + ((TextView) header.findViewById(R.id.settings_notifications_title)) + .setTextColor(style.getTextPrimaryColor()); + ((TextView) header.findViewById(R.id.addMemberAction)) + .setTextColor(style.getTextPrimaryColor()); + members.setTextColor(style.getTextPrimaryColor()); + administrationAction.setTextColor(style.getTextPrimaryColor()); + leaveAction.setTextColor(style.getTextDangerColor()); + + if (groupVM.getGroupType() == GroupType.CHANNEL) { + leaveAction.setText(R.string.group_leave_channel); } else { - UserVM admin = users().get(groupInfo.getCreatorId()); - bind(admin.getName(), new ValueChangedListener() { - @Override - public void onChanged(String val, Value Value) { - createdBy.setText(getString(R.string.group_created_by).replace("{0}", val)); - } - }); + leaveAction.setText(R.string.group_leave); } - //Description - theme = new String[1]; - about = new String[1]; - // TextView themeTV = (TextView) header.findViewById(R.id.theme); - // themeTV.setTextColor(style.getTextPrimaryColor()); - // ((TextView) header.findViewById(R.id.group_theme_hint)).setTextColor(style.getTextSecondaryColor()); - TextView aboutTV = (TextView) header.findViewById(R.id.about); - aboutTV.setTextColor(style.getTextPrimaryColor()); - ((TextView) header.findViewById(R.id.about_hint)).setTextColor(style.getTextSecondaryColor()); - final View descriptionContainer = header.findViewById(R.id.descriptionContainer); - final TextView themeHeader = (TextView) header.findViewById(R.id.theme_header); - themeHeader.setTextColor(style.getProfileSubtitleColor()); - - final boolean finalIsAdmin = isAdmin; -// bind(themeTV, header.findViewById(R.id.themeContainer), groupInfo.getTheme(), new ActorBinder.OnChangedListener() { -// @Override -// public void onChanged(String s) { -// theme[0] = s; -// updateDescriptionVisibility(descriptionContainer, finalIsAdmin, header); -// } -// }, !isAdmin, getString(R.string.theme_group_empty)); - - //bind(themeHeader, themeHeader, groupInfo.getTheme()); - - bind(aboutTV, header.findViewById(R.id.aboutContainer), groupInfo.getAbout(), new ActorBinder.OnChangedListener() { - @Override - public void onChanged(String s) { - about[0] = s; - updateDescriptionVisibility(descriptionContainer, finalIsAdmin, header); + header.findViewById(R.id.after_settings_divider).setBackgroundColor(style.getBackyardBackgroundColor()); + + // + // Header + // + avatarView.bind(groupVM.getAvatar().get(), groupVM.getName().get(), groupVM.getId()); + avatarView.setOnClickListener(view -> { + if (groupVM.getAvatar().get() != null) { + startActivity(ViewAvatarActivity.viewGroupAvatar(chatId, getActivity())); } - }, !isAdmin, getString(R.string.about_group_empty)); - - if (isAdmin) { -// header.findViewById(R.id.themeContainer).setOnClickListener(new View.OnClickListener() { -// @Override -// public void onClick(View v) { -// startActivity(Intents.editGroupTheme(groupInfo.getId(), getActivity())); -// } -// }); - - header.findViewById(R.id.aboutContainer).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - startActivity(Intents.editGroupAbout(groupInfo.getId(), getActivity())); - } - }); - } - // Settings + }); + bind(groupVM.getName(), name -> { + title.setText(name); + }); + bind(groupVM.getMembersCount(), val -> { + subtitle.setText(messenger().getFormatter().formatGroupMembers(val)); + }); - final SwitchCompat isNotificationsEnabled = (SwitchCompat) header.findViewById(R.id.enableNotifications); - isNotificationsEnabled.setChecked(messenger().isNotificationsEnabled(Peer.group(chatId))); - isNotificationsEnabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - messenger().changeNotificationsEnabled(Peer.group(chatId), isChecked); + // About + bind(groupVM.getAbout(), (about) -> { + aboutTV.setText(about); + aboutTV.setVisibility(about != null ? View.VISIBLE : View.GONE); + }); + bind(groupVM.getShortName(), shortName -> { + if (shortName != null) { + shortNameView.setText("@" + shortName); + + String prefix = ActorSDK.sharedActor().getGroupInvitePrefix(); + if (prefix != null) { + shortLinkView.setText(prefix + shortName); + shortLinkView.setVisibility(View.VISIBLE); + } else { + shortLinkView.setVisibility(View.GONE); + } } + shortNameCont.setVisibility(shortName != null ? View.VISIBLE : View.GONE); }); - header.findViewById(R.id.notificationsCont).setOnClickListener(new View.OnClickListener() { + final ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); + shortNameCont.setOnClickListener(new View.OnClickListener() { @Override - public void onClick(View v) { - isNotificationsEnabled.setChecked(!isNotificationsEnabled.isChecked()); + public void onClick(View view) { + String link = shortLinkView.getText().toString(); + clipboard.setPrimaryClip(ClipData.newPlainText(null, (link.contains("://") ? "" : "https://") + link)); + Toast.makeText(getActivity(), getString(R.string.invite_link_copied), Toast.LENGTH_SHORT).show(); } }); + bind(groupVM.getAbout(), groupVM.getShortName(), (about, shortName) -> { + descriptionContainer.setVisibility(about != null || shortName != null + ? View.VISIBLE + : View.GONE); + }); - //Members - TextView memberCount = (TextView) header.findViewById(R.id.membersCount); - memberCount.setText(groupInfo.getMembersCount() + ""); - memberCount.setTextColor(style.getTextHintColor()); - listView.addHeaderView(header, null, false); - - View add = inflater.inflate(R.layout.fragment_group_add, listView, false); - add.findViewById(R.id.bottom_divider).setBackgroundColor(style.getBackyardBackgroundColor()); - TextView name = (TextView) add.findViewById(R.id.name); - name.setTextColor(style.getActionAddContactColor()); - name.setTypeface(Fonts.medium()); - TintImageView addIcon = (TintImageView) add.findViewById(R.id.add_icon); - addIcon.setTint(style.getGroupActionAddIconColor()); - addIcon.setTint(style.getActionAddContactColor()); + // Notifications + isNotificationsEnabled.setChecked(messenger().isNotificationsEnabled(Peer.group(chatId))); + isNotificationsEnabled.setOnCheckedChangeListener((buttonView, isChecked) -> { + messenger().changeNotificationsEnabled(Peer.group(chatId), isChecked); + }); + header.findViewById(R.id.notificationsCont).setOnClickListener(v -> { + isNotificationsEnabled.setChecked(!isNotificationsEnabled.isChecked()); + }); - add.findViewById(R.id.addUser).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - startActivity(new Intent(getActivity(), AddMemberActivity.class) - .putExtra("GROUP_ID", chatId)); + // Add Member + bind(groupVM.getIsCanInviteMembers(), (canInvite) -> { + if (canInvite) { + addMember.setVisibility(View.VISIBLE); + } else { + addMember.setVisibility(View.GONE); } }); - listView.addFooterView(add, null, false); + addMember.setOnClickListener(view -> { + startActivity(new Intent(getActivity(), AddMemberActivity.class) + .putExtra(Intents.EXTRA_GROUP_ID, chatId)); + }); - groupUserAdapter = new MembersAdapter(groupInfo.getMembers().get(), getActivity()); - bind(groupInfo.getMembers(), new ValueChangedListener>() { - @Override - public void onChanged(HashSet val, Value> Value) { - groupUserAdapter.updateUid(val); + // Administration + if (groupVM.getIsCanEditAdministration().get() || groupVM.getIsCanDelete().get()) { + administrationAction.setOnClickListener(view -> { + startActivity(new Intent(getActivity(), GroupAdminActivity.class) + .putExtra(Intents.EXTRA_GROUP_ID, chatId)); + }); + } else { + administrationAction.setVisibility(View.GONE); + } + + // Async Members + // Showing member only when members available and async members is enabled + bind(groupVM.getIsCanViewMembers(), groupVM.getIsAsyncMembers(), (canViewMembers, vm1, isAsync, vm2) -> { + if (canViewMembers) { + if (isAsync) { + members.setVisibility(View.VISIBLE); + header.findViewById(R.id.after_settings_divider).setVisibility(View.GONE); + } else { + members.setVisibility(View.GONE); + header.findViewById(R.id.after_settings_divider).setVisibility(View.VISIBLE); + } + } else { + members.setVisibility(View.GONE); + header.findViewById(R.id.after_settings_divider).setVisibility(View.GONE); } }); - listView.setAdapter(groupUserAdapter); + members.setOnClickListener(view -> { + startActivity(new Intent(getContext(), MembersActivity.class) + .putExtra(Intents.EXTRA_GROUP_ID, groupVM.getId())); + }); - listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - Object item = parent.getItemAtPosition(position); - if (item != null && item instanceof GroupMember) { - final GroupMember groupMember = (GroupMember) item; - if (groupMember.getUid() == myUid()) { - return; - } - final UserVM userVM = users().get(groupMember.getUid()); - if (userVM == null) { - return; - } + // Leave + bind(groupVM.getIsCanLeave(), canLeave -> { + if (canLeave) { + leaveAction.setVisibility(View.VISIBLE); + leaveAction.setOnClickListener(view1 -> { new AlertDialog.Builder(getActivity()) - .setItems(new CharSequence[]{ - getString(R.string.group_context_message).replace("{0}", userVM.getName().get()), - getString(R.string.group_context_call).replace("{0}", userVM.getName().get()), - getString(R.string.group_context_view).replace("{0}", userVM.getName().get()), - getString(R.string.group_context_remove).replace("{0}", userVM.getName().get()), - }, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == 0) { - startActivity(Intents.openPrivateDialog(userVM.getId(), true, getActivity())); - } else if (which == 1) { - final ArrayList phones = userVM.getPhones().get(); - if (phones.size() == 0) { - Toast.makeText(getActivity(), "No phones available", Toast.LENGTH_SHORT).show(); - } else if (phones.size() == 1) { - startActivity(Intents.call(phones.get(0).getPhone())); - } else { - CharSequence[] sequences = new CharSequence[phones.size()]; - for (int i = 0; i < sequences.length; i++) { - try { - Phonenumber.PhoneNumber number = PhoneNumberUtil.getInstance().parse("+" + phones.get(i).getPhone(), "us"); - sequences[i] = phones.get(which).getTitle() + ": " + PhoneNumberUtil.getInstance().format(number, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL); - } catch (NumberParseException e) { - e.printStackTrace(); - sequences[i] = phones.get(which).getTitle() + ": +" + phones.get(i).getPhone(); - } - } - new AlertDialog.Builder(getActivity()) - .setItems(sequences, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - startActivity(Intents.call(phones.get(which).getPhone())); - } - }) - .show() - .setCanceledOnTouchOutside(true); - } - } else if (which == 2) { - ActorSDKLauncher.startProfileActivity(getActivity(), userVM.getId()); - } else if (which == 3) { - new AlertDialog.Builder(getActivity()) - .setMessage(getString(R.string.alert_group_remove_text).replace("{0}", userVM.getName().get())) - .setPositiveButton(R.string.alert_group_remove_yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog2, int which) { - execute(messenger().kickMember(chatId, userVM.getId()), - R.string.progress_common, new CommandCallback() { - @Override - public void onResult(Void res) { - - } - - @Override - public void onError(Exception e) { - Toast.makeText(getActivity(), R.string.toast_unable_kick, Toast.LENGTH_SHORT).show(); - } - }); - } - }) - .setNegativeButton(R.string.dialog_cancel, null) - .show() - .setCanceledOnTouchOutside(true); - } - } + .setMessage(getString(groupVM.getGroupType() == GroupType.CHANNEL ? R.string.alert_leave_channel_message : R.string.alert_leave_group_message).replace("%1$s", + groupVM.getName().get())) + .setPositiveButton(R.string.alert_leave_group_yes, (dialog2, which) -> { + execute(messenger().leaveAndDeleteGroup(chatId).then(aVoid -> ActorSDK.returnToRoot(getActivity()))); }) + .setNegativeButton(R.string.dialog_cancel, null) .show() .setCanceledOnTouchOutside(true); + }); + } else { + leaveAction.setVisibility(View.GONE); + } + }); + + + listView.addHeaderView(header, null, false); + + // + // Members + // + + groupUserAdapter = new MembersAdapter(getActivity(), getArguments().getInt("groupId")); + + listView.setAdapter(groupUserAdapter); + listView.setOnItemClickListener((parent, view, position, id) -> { + Object item = parent.getItemAtPosition(position); + if (item != null && item instanceof GroupMember) { + GroupMember groupMember = (GroupMember) item; + if (groupMember.getUid() != myUid()) { + UserVM userVM = users().get(groupMember.getUid()); + if (userVM != null) { + startActivity(Intents.openPrivateDialog(userVM.getId(), true, getActivity())); + } + } + } + }); + listView.setOnItemLongClickListener((adapterView, view, i, l) -> { + Object item = adapterView.getItemAtPosition(i); + if (item != null && item instanceof GroupMember) { + GroupMember groupMember = (GroupMember) item; + if (groupMember.getUid() != myUid()) { + UserVM userVM = users().get(groupMember.getUid()); + if (userVM != null) { + groupUserAdapter.onMemberClick(groupVM, userVM, groupMember.isAdministrator(), groupMember.getInviterUid() == myUid(), (BaseActivity) getActivity()); + return true; + } } } + return false; }); + // + // Scroll handling + // + listView.setOnScrollListener(new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { @@ -348,53 +346,68 @@ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCoun } }); - res.findViewById(R.id.after_about_divider).setBackgroundColor(style.getBackyardBackgroundColor()); - res.findViewById(R.id.after_settings_divider).setBackgroundColor(style.getBackyardBackgroundColor()); - res.findViewById(R.id.bottom_divider).setBackgroundColor(style.getBackyardBackgroundColor()); + // + // Placeholder + // + bind(groupVM.isMember(), (isMember) -> { + notMemberView.setVisibility(isMember ? View.GONE : View.VISIBLE); + getActivity().invalidateOptionsMenu(); + }); - TextView settingsHeaderText = (TextView) res.findViewById(R.id.settings_header_text); - settingsHeaderText.setTextColor(style.getSettingsCategoryTextColor()); - - TintImageView notificationSettingIcon = (TintImageView) res.findViewById(R.id.settings_notification_icon); - notificationSettingIcon.setTint(style.getSettingsIconColor()); - ((TextView) res.findViewById(R.id.settings_notifications_title)).setTextColor(style.getTextPrimaryColor()); - -// TintImageView shareMediaIcon = (TintImageView) res.findViewById(R.id.share_media_icon); -// shareMediaIcon.setTint(style.getSettingsIconColor()); -// ((TextView) res.findViewById(R.id.settings_media_title)).setTextColor(style.getTextPrimaryColor()); -// ((TextView) res.findViewById(R.id.mediaCount)).setTextColor(style.getTextHintColor()); -// -// TintImageView shareDocsIcon = (TintImageView) res.findViewById(R.id.share_docs_icon); -// shareDocsIcon.setTint(style.getSettingsIconColor()); -// ((TextView) res.findViewById(R.id.share_docs_title)).setTextColor(style.getTextPrimaryColor()); -// ((TextView) res.findViewById(R.id.docCount)).setTextColor(style.getTextHintColor()); - -// TextView sharedHeaderText = (TextView) res.findViewById(R.id.shared_header_text); -// sharedHeaderText.setTextColor(style.getSettingsCategoryTextColor()); - - TextView membersHeaderText = (TextView) res.findViewById(R.id.membersTitle); - membersHeaderText.setTextColor(style.getSettingsCategoryTextColor()); + // Menu + bind(groupVM.getIsCanEditInfo(), canEditInfo -> { + getActivity().invalidateOptionsMenu(); + }); return res; } - public void updateDescriptionVisibility(View descriptionContainer, boolean finalIsAdmin, View header) { - // View themeDivider = header.findViewById(R.id.themeDivider); - // themeDivider.setBackgroundColor(ActorSDK.sharedActor().style.getDividerColor()); + @Override + public void onResume() { + super.onResume(); + memberBindings = bind(groupVM.getIsAsyncMembers(), groupVM.getMembers(), (isAsyncMembers, valueModel, memberList, valueModel2) -> { + if (isAsyncMembers) { + groupUserAdapter.setMembers(new ArrayList<>()); + } else { + groupUserAdapter.setMembers(memberList); + } + }); + } - boolean themeVis = theme[0] != null && !theme[0].isEmpty(); - boolean aboutVis = about[0] != null && !about[0].isEmpty(); + @Override + public void onPause() { + super.onPause(); + if (memberBindings != null) { + for (ActorBinder.Binding b : memberBindings) { + getBINDER().unbind(b); + } + } + } - descriptionContainer.setVisibility((aboutVis || themeVis || finalIsAdmin) ? View.VISIBLE : View.GONE); - // themeDivider.setVisibility(((themeVis && aboutVis) || isAdmin) ? View.VISIBLE : View.GONE); + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { + super.onCreateOptionsMenu(menu, menuInflater); + if (groupVM.getIsCanEditInfo().get()) { + MenuItem menuItem = menu.add(Menu.NONE, R.id.edit, Menu.NONE, R.string.actor_menu_edit); + menuItem.setIcon(R.drawable.ic_edit_white_24dp); + menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + } + } + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.edit) { + startActivity(new Intent(getContext(), GroupEditActivity.class) + .putExtra(Intents.EXTRA_GROUP_ID, groupVM.getId())); + return true; + } else { + return super.onOptionsItemSelected(item); + } } public void updateBar(int offset) { - avatarView.setOffset(offset); - int baseColor = getResources().getColor(R.color.primary); ActorStyle style = ActorSDK.sharedActor().style; if (style.getToolBarColor() != 0) { @@ -417,86 +430,13 @@ public void updateBar(int offset) { } } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { - super.onCreateOptionsMenu(menu, menuInflater); - if (groupInfo.isMember().get()) { - menuInflater.inflate(R.menu.group_info, menu); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == R.id.leaveGroup) { - new AlertDialog.Builder(getActivity()) - .setMessage(getString(R.string.alert_leave_group_message).replace("%1$s", - groupInfo.getName().get())) - .setPositiveButton(R.string.alert_leave_group_yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog2, int which) { - execute(messenger().leaveGroup(chatId)); - } - }) - .setNegativeButton(R.string.dialog_cancel, null) - .show() - .setCanceledOnTouchOutside(true); - - return true; - } else if (item.getItemId() == R.id.addMember) { - startActivity(new Intent(getActivity(), AddMemberActivity.class) - .putExtra("GROUP_ID", chatId)); - } else if (item.getItemId() == R.id.editTitle) { - startActivity(Intents.editGroupTitle(chatId, getActivity())); - } else if (item.getItemId() == R.id.changePhoto) { - startActivity(ViewAvatarActivity.viewGroupAvatar(chatId, getActivity())); - } - return super.onOptionsItemSelected(item); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (resultCode == Activity.RESULT_OK && requestCode == 0 && data != null && data.hasExtra(Intents.EXTRA_UID)) { - final UserVM userModel = users().get(data.getIntExtra(Intents.EXTRA_UID, 0)); - - for (GroupMember uid : groupInfo.getMembers().get()) { - if (uid.getUid() == userModel.getId()) { - Toast.makeText(getActivity(), R.string.toast_already_member, Toast.LENGTH_SHORT).show(); - return; - } - } - - new AlertDialog.Builder(getActivity()) - .setMessage(getString(R.string.alert_group_add_text).replace("{0}", userModel.getName().get())) - .setPositiveButton(R.string.alert_group_add_yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog2, int which) { - execute(messenger().inviteMember(chatId, userModel.getId()), - R.string.progress_common, new CommandCallback() { - @Override - public void onResult(Void res) { - - } - - @Override - public void onError(Exception e) { - Toast.makeText(getActivity(), R.string.toast_unable_add, Toast.LENGTH_SHORT).show(); - } - }); - } - }) - .setNegativeButton(R.string.dialog_cancel, null) - .show() - .setCanceledOnTouchOutside(true); - } else { - super.onActivityResult(requestCode, resultCode, data); - } - } - @Override public void onDestroyView() { super.onDestroyView(); - groupUserAdapter.dispose(); - groupUserAdapter = null; + if (groupUserAdapter != null) { + groupUserAdapter.dispose(); + groupUserAdapter = null; + } if (avatarView != null) { avatarView.unbind(); avatarView = null; diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupPermissionsActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupPermissionsActivity.java new file mode 100644 index 0000000000..cb2a61b5a1 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupPermissionsActivity.java @@ -0,0 +1,19 @@ +package im.actor.sdk.controllers.group; + +import android.os.Bundle; + +import im.actor.sdk.controllers.Intents; +import im.actor.sdk.controllers.activity.BaseFragmentActivity; + +public class GroupPermissionsActivity extends BaseFragmentActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState == null) { + showFragment(GroupPermissionsFragment.create( + getIntent().getIntExtra(Intents.EXTRA_GROUP_ID, 0)), false); + } + } +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupPermissionsFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupPermissionsFragment.java new file mode 100644 index 0000000000..ce659f76c7 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupPermissionsFragment.java @@ -0,0 +1,173 @@ +package im.actor.sdk.controllers.group; + +import android.app.Activity; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.TextView; + +import fr.castorflex.android.circularprogressbar.CircularProgressBar; +import im.actor.core.entity.GroupPermissions; +import im.actor.core.entity.GroupType; +import im.actor.sdk.R; +import im.actor.sdk.controllers.BaseFragment; + +import static im.actor.sdk.util.ActorSDKMessenger.messenger; + +public class GroupPermissionsFragment extends BaseFragment { + + public static GroupPermissionsFragment create(int chatId) { + Bundle args = new Bundle(); + args.putInt("groupId", chatId); + GroupPermissionsFragment res = new GroupPermissionsFragment(); + res.setArguments(args); + return res; + } + + private GroupPermissions permissions; + private int groupId; + + private CircularProgressBar progress; + private View scrollContainer; + + private TextView canEditInfoTV; + private CheckBox canEditInfo; + + private TextView canAdminsEditInfoTV; + private CheckBox canAdminsEditInfo; + + private TextView canSendInvitationsTV; + private CheckBox canSendInvitations; + + private TextView showLeaveJoinTV; + private CheckBox showLeaveJoin; + + private TextView showAdminsToMembersTV; + private CheckBox showAdminsToMembers; + + boolean isChannel = false; + + public GroupPermissionsFragment() { + setRootFragment(true); + setHomeAsUp(true); + setShowHome(true); + } + + @Override + public void onCreate(Bundle saveInstance) { + super.onCreate(saveInstance); + groupId = getArguments().getInt("groupId"); + if (messenger().getGroup(groupId).getGroupType() == GroupType.CHANNEL) { + setTitle(R.string.channel_admin_title); + isChannel = true; + } else { + setTitle(R.string.group_admin_title); + } + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View res = inflater.inflate(R.layout.fragment_edit_permissions, container, false); + View rootContainer = res.findViewById(R.id.rootContainer); + rootContainer.setBackgroundColor(style.getBackyardBackgroundColor()); + + canEditInfo = (CheckBox) res.findViewById(R.id.canEditValue); + canEditInfoTV = (TextView) res.findViewById(R.id.canEditTitle); + canEditInfoTV.setText(isChannel ? R.string.channel_can_edit_info_members : R.string.group_can_edit_info_members); + + canAdminsEditInfo = (CheckBox) res.findViewById(R.id.canAdminsEditValue); + canAdminsEditInfoTV = (TextView) res.findViewById(R.id.canAdminsEditTitle); + canAdminsEditInfoTV.setText(isChannel ? R.string.channel_can_edit_info_admins : R.string.group_can_edit_info_admins); + + canSendInvitations = (CheckBox) res.findViewById(R.id.canMembersInviteValue); + canSendInvitationsTV = (TextView) res.findViewById(R.id.canMembersInviteTitle); + canSendInvitationsTV.setText(isChannel ? R.string.channel_can_invite_members : R.string.group_can_invite_members); + + if (!isChannel) { + showLeaveJoin = (CheckBox) res.findViewById(R.id.showJoinLeaveValue); + showLeaveJoinTV = (TextView) res.findViewById(R.id.showJoinLeaveTitle); + showLeaveJoinTV.setText(isChannel ? R.string.channel_show_leave_join : R.string.group_show_leave_join); + } else { + res.findViewById(R.id.showJoinLeaveContainer).setVisibility(View.GONE); + } + + showAdminsToMembers = (CheckBox) res.findViewById(R.id.showAdminsToMembersValue); + showAdminsToMembersTV = (TextView) res.findViewById(R.id.showAdminsToMembersTitle); + showAdminsToMembersTV.setText(isChannel ? R.string.channel_show_admin_to_members : R.string.group_show_admin_to_members); + + scrollContainer = res.findViewById(R.id.scrollContainer); + progress = (CircularProgressBar) res.findViewById(R.id.progress); + progress.setIndeterminate(true); + return res; + } + + @Override + public void onResume() { + super.onResume(); + if (permissions == null) { + progress.setVisibility(View.VISIBLE); + scrollContainer.setVisibility(View.GONE); + wrap(messenger().loadGroupPermissions(groupId)).then(r -> { + goneView(progress); + showView(scrollContainer); + this.permissions = r; + bindView(); + }); + } else { + progress.setVisibility(View.GONE); + scrollContainer.setVisibility(View.VISIBLE); + } + } + + public void bindView() { + Activity activity = getActivity(); + if (activity != null) { + activity.invalidateOptionsMenu(); + } + canEditInfo.setChecked(permissions.isMembersCanEditInfo()); + canAdminsEditInfo.setChecked(permissions.isAdminsCanEditGroupInfo()); + canSendInvitations.setChecked(permissions.isMembersCanInvite()); + showLeaveJoin.setChecked(permissions.isShowJoinLeaveMessages()); + showAdminsToMembers.setChecked(permissions.isShowAdminsToMembers()); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + if (permissions != null) { + inflater.inflate(R.menu.next, menu); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.next) { + if (permissions.isMembersCanEditInfo() != canEditInfo.isChecked() || + permissions.isAdminsCanEditGroupInfo() != canAdminsEditInfo.isChecked() || + permissions.isMembersCanInvite() != canSendInvitations.isChecked() || + permissions.isShowJoinLeaveMessages() != showLeaveJoin.isChecked() || + permissions.isShowAdminsToMembers() != showAdminsToMembers.isChecked()) { + permissions.setMembersCanEditInfo(canEditInfo.isChecked()); + permissions.setAdminsCanEditGroupInfo(canAdminsEditInfo.isChecked()); + permissions.setShowJoinLeaveMessages(showLeaveJoin.isChecked()); + permissions.setMembersCanInvite(canSendInvitations.isChecked()); + permissions.setShowAdminsToMembers(showAdminsToMembers.isChecked()); + execute(messenger().saveGroupPermissions(groupId, permissions).then(r -> { + finishActivity(); + })); + } else { + finishActivity(); + } + return true; + } else { + return super.onOptionsItemSelected(item); + } + } +} \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupTypeActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupTypeActivity.java new file mode 100644 index 0000000000..03574cc918 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupTypeActivity.java @@ -0,0 +1,19 @@ +package im.actor.sdk.controllers.group; + +import android.os.Bundle; + +import im.actor.sdk.controllers.Intents; +import im.actor.sdk.controllers.activity.BaseFragmentActivity; + +public class GroupTypeActivity extends BaseFragmentActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState == null) { + showFragment(GroupTypeFragment.create( + getIntent().getIntExtra(Intents.EXTRA_GROUP_ID, 0), false), false); + } + } +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupTypeFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupTypeFragment.java new file mode 100644 index 0000000000..8f9337cc89 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/GroupTypeFragment.java @@ -0,0 +1,186 @@ +package im.actor.sdk.controllers.group; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.RadioButton; +import android.widget.TextView; +import android.widget.Toast; + +import im.actor.core.entity.GroupType; +import im.actor.core.viewmodel.GroupVM; +import im.actor.sdk.ActorSDK; +import im.actor.sdk.R; +import im.actor.sdk.controllers.BaseFragment; +import im.actor.sdk.controllers.compose.CreateGroupActivity; +import im.actor.sdk.controllers.compose.GroupUsersFragment; + +import static im.actor.sdk.util.ActorSDKMessenger.messenger; + +public class GroupTypeFragment extends BaseFragment { + + public static GroupTypeFragment create(int groupId, boolean isCreate) { + Bundle bundle = new Bundle(); + bundle.putInt("groupId", groupId); + bundle.putBoolean("isCreate", isCreate); + GroupTypeFragment editFragment = new GroupTypeFragment(); + editFragment.setArguments(bundle); + return editFragment; + } + + private EditText publicShortName; + private GroupVM groupVM; + private boolean isPublic; + private boolean isCreate; + + public GroupTypeFragment() { + setRootFragment(true); + setHomeAsUp(true); + setShowHome(true); + } + + @Override + public void onCreate(Bundle saveInstance) { + super.onCreate(saveInstance); + isCreate = getArguments().getBoolean("isCreate", false); + groupVM = messenger().getGroup(getArguments().getInt("groupId")); + setTitle(groupVM.getGroupType() == GroupType.CHANNEL ? R.string.channel_title : R.string.group_title); + + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View res = inflater.inflate(R.layout.fragment_edit_type, container, false); + res.setBackgroundColor(style.getBackyardBackgroundColor()); + TextView publicTitle = (TextView) res.findViewById(R.id.publicTitle); + publicTitle.setText(groupVM.getGroupType() == GroupType.CHANNEL ? R.string.group_public_channel_title : R.string.group_public_group_title); + publicTitle.setTextColor(style.getTextPrimaryColor()); + TextView publicDescription = (TextView) res.findViewById(R.id.publicDescription); + publicDescription.setText(groupVM.getGroupType() == GroupType.CHANNEL ? R.string.group_public_channel_text : R.string.group_public_group_text); + publicDescription.setTextColor(style.getTextSecondaryColor()); + TextView privateTitle = (TextView) res.findViewById(R.id.privateTitle); + privateTitle.setText(groupVM.getGroupType() == GroupType.CHANNEL ? R.string.group_private_channel_title : R.string.group_private_group_title); + privateTitle.setTextColor(style.getTextPrimaryColor()); + TextView privateDescription = (TextView) res.findViewById(R.id.privateDescription); + privateDescription.setText(groupVM.getGroupType() == GroupType.CHANNEL ? R.string.group_private_channel_text : R.string.group_private_group_text); + privateDescription.setTextColor(style.getTextSecondaryColor()); + TextView publicLinkPrefix = (TextView) res.findViewById(R.id.publicLinkPrefix); + publicLinkPrefix.setTextColor(style.getTextSecondaryColor()); + String prefix = ActorSDK.sharedActor().getGroupInvitePrefix(); + if (prefix == null) { + prefix = "@"; + } + publicLinkPrefix.setText(prefix); + RadioButton publicRadio = (RadioButton) res.findViewById(R.id.publicRadio); + RadioButton privateRadio = (RadioButton) res.findViewById(R.id.privateRadio); + View publicSelector = res.findViewById(R.id.publicSelector); + View privateSelector = res.findViewById(R.id.privateSelector); + publicShortName = (EditText) res.findViewById(R.id.publicLink); + View publicLinkContainer = res.findViewById(R.id.publicContainer); + View publicShadowTop = res.findViewById(R.id.shadowTop); + View publicShadowBottom = res.findViewById(R.id.shadowBottom); + + if (groupVM.getShortName().get() != null) { + publicRadio.setChecked(true); + privateRadio.setChecked(false); + publicLinkContainer.setVisibility(View.VISIBLE); + publicShadowTop.setVisibility(View.VISIBLE); + publicShadowBottom.setVisibility(View.VISIBLE); + publicShortName.setText(groupVM.getShortName().get()); + isPublic = true; + } else { + publicRadio.setChecked(false); + privateRadio.setChecked(true); + publicLinkContainer.setVisibility(View.GONE); + publicShadowTop.setVisibility(View.GONE); + publicShadowBottom.setVisibility(View.GONE); + publicShortName.setText(null); + isPublic = false; + } + View.OnClickListener publicClick = view -> { + if (!isPublic) { + isPublic = true; + publicRadio.setChecked(true); + privateRadio.setChecked(false); + publicLinkContainer.setVisibility(View.VISIBLE); + publicShadowTop.setVisibility(View.VISIBLE); + publicShadowBottom.setVisibility(View.VISIBLE); + publicShortName.setText(groupVM.getShortName().get()); + } + }; + View.OnClickListener privateClick = view -> { + if (isPublic) { + isPublic = false; + publicRadio.setChecked(false); + privateRadio.setChecked(true); + publicLinkContainer.setVisibility(View.GONE); + publicShadowTop.setVisibility(View.GONE); + publicShadowBottom.setVisibility(View.GONE); + publicShortName.setText(null); + } + }; + publicRadio.setOnClickListener(publicClick); + publicSelector.setOnClickListener(publicClick); + privateRadio.setOnClickListener(privateClick); + privateSelector.setOnClickListener(privateClick); + + return res; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.next, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.next) { + if (isPublic) { + String nShortName = publicShortName.getText().toString().trim(); + if (nShortName.length() == 0) { + Toast.makeText(getActivity(), R.string.group_edit_change_short_name_error, Toast.LENGTH_SHORT).show(); + return true; + } + if (nShortName.equals(groupVM.getShortName().get())) { + onEditShortNameSuccess(); + return true; + } + execute(messenger().editGroupShortName(groupVM.getId(), nShortName).then(r -> { + onEditShortNameSuccess(); + }).failure(e -> { + Toast.makeText(getActivity(), R.string.group_edit_change_short_name_error, Toast.LENGTH_SHORT).show(); + })); + } else { + if (groupVM.getShortName().get() == null) { + onEditShortNameSuccess(); + return true; + } else { + execute(messenger().editGroupShortName(groupVM.getId(), null).then(r -> { + onEditShortNameSuccess(); + }).failure(e -> { + Toast.makeText(getActivity(), R.string.group_edit_change_short_name_error, Toast.LENGTH_SHORT).show(); + })); + } + } + return true; + } + return super.onOptionsItemSelected(item); + } + + protected void onEditShortNameSuccess() { + if (isCreate) { + ((CreateGroupActivity) getActivity()).showNextFragment( + GroupUsersFragment.createChannel(getArguments().getInt("groupId")), false); + } else { + finishActivity(); + } + } +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/InviteLinkActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/InviteLinkActivity.java index eadba566b3..42ae2d958f 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/InviteLinkActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/InviteLinkActivity.java @@ -17,7 +17,7 @@ protected void onCreate(Bundle savedInstanceState) { int chatId = getIntent().getIntExtra(Intents.EXTRA_GROUP_ID, 0); if (savedInstanceState == null) { - showFragment(InviteLinkFragment.create(chatId), false, false); + showFragment(InviteLinkFragment.create(chatId), false); } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/MembersActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/MembersActivity.java new file mode 100644 index 0000000000..8f834462a1 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/MembersActivity.java @@ -0,0 +1,18 @@ +package im.actor.sdk.controllers.group; + +import android.os.Bundle; + +import im.actor.sdk.controllers.Intents; +import im.actor.sdk.controllers.activity.BaseFragmentActivity; + +public class MembersActivity extends BaseFragmentActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState == null) { + int groupId = getIntent().getIntExtra(Intents.EXTRA_GROUP_ID, 0); + showFragment(MembersFragment.create(groupId), false); + } + } +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/MembersFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/MembersFragment.java new file mode 100644 index 0000000000..4584c2b494 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/MembersFragment.java @@ -0,0 +1,180 @@ +package im.actor.sdk.controllers.group; + +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import fr.castorflex.android.circularprogressbar.CircularProgressBar; +import im.actor.core.entity.GroupMember; +import im.actor.core.entity.GroupType; +import im.actor.core.viewmodel.GroupVM; +import im.actor.core.viewmodel.UserVM; +import im.actor.sdk.ActorSDK; +import im.actor.sdk.R; +import im.actor.sdk.controllers.BaseFragment; +import im.actor.sdk.controllers.Intents; +import im.actor.sdk.controllers.activity.BaseActivity; +import im.actor.sdk.controllers.group.view.MembersAdapter; +import im.actor.sdk.util.Screen; +import im.actor.sdk.view.DividerView; +import im.actor.sdk.view.adapters.RecyclerListView; + +import static im.actor.sdk.util.ActorSDKMessenger.groups; +import static im.actor.sdk.util.ActorSDKMessenger.myUid; +import static im.actor.sdk.util.ActorSDKMessenger.users; + +public class MembersFragment extends BaseFragment { + + protected CircularProgressBar progressView; + private LinearLayout footer; + + public static MembersFragment create(int groupId) { + MembersFragment res = new MembersFragment(); + Bundle args = new Bundle(); + args.putInt("groupId", groupId); + res.setArguments(args); + return res; + } + + private MembersAdapter adapter; + + public MembersFragment() { + setRootFragment(true); + setTitle(R.string.group_members_header); + setHomeAsUp(true); + setShowHome(true); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View res = inflater.inflate(R.layout.fragment_members, container, false); + int groupId = getArguments().getInt("groupId"); + adapter = new MembersAdapter(getContext(), groupId); + + GroupVM groupVM = groups().get(groupId); + RecyclerListView list = (RecyclerListView) res.findViewById(R.id.items); + + + Boolean canInvite = groupVM.getIsCanInviteMembers().get(); + Boolean canInviteViaLink = groupVM.getIsCanInviteViaLink().get(); + if (canInvite || canInviteViaLink) { + LinearLayout header = new LinearLayout(getActivity()); + list.addHeaderView(header); + + if (canInvite) { + TextView addMmemberTV = new TextView(getContext()); + addMmemberTV.setBackgroundResource(R.drawable.selector); + addMmemberTV.setTextSize(16); + addMmemberTV.setPadding(Screen.dp(72), 0, 0, 0); + addMmemberTV.setGravity(Gravity.CENTER_VERTICAL); + addMmemberTV.setText(groupVM.getGroupType() == GroupType.CHANNEL ? R.string.channel_add_member : R.string.group_add_member); + addMmemberTV.setTextColor(ActorSDK.sharedActor().style.getTextPrimaryColor()); + addMmemberTV.setOnClickListener(view -> { + startActivity(new Intent(getActivity(), AddMemberActivity.class) + .putExtra(Intents.EXTRA_GROUP_ID, groupId)); + }); + + header.addView(addMmemberTV, ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(58)); + } + + if (canInvite && canInviteViaLink) { + header.addView(new DividerView(getActivity()), ViewGroup.LayoutParams.MATCH_PARENT, 1); + } + + if (canInviteViaLink) { + TextView shareLinkTV = new TextView(getContext()); + shareLinkTV.setBackgroundResource(R.drawable.selector); + shareLinkTV.setTextSize(16); + shareLinkTV.setPadding(Screen.dp(72), 0, 0, 0); + shareLinkTV.setGravity(Gravity.CENTER_VERTICAL); + shareLinkTV.setTextColor(ActorSDK.sharedActor().style.getTextPrimaryColor()); + shareLinkTV.setText(R.string.invite_link_action_share); + shareLinkTV.setOnClickListener(view -> Intents.inviteLink(groupId, getActivity())); + + header.addView(shareLinkTV, ViewGroup.LayoutParams.MATCH_PARENT, Screen.dp(58)); + } + } + + footer = new LinearLayout(getActivity()); + footer.setVisibility(View.INVISIBLE); + list.addFooterView(footer); + CircularProgressBar botProgressView = new CircularProgressBar(getActivity()); + int padding = Screen.dp(16); + botProgressView.setPadding(padding, padding, padding, padding); + botProgressView.setIndeterminate(true); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(Screen.dp(72), Screen.dp(72)); + params.gravity = Gravity.CENTER; + FrameLayout cont = new FrameLayout(getActivity()); + cont.addView(botProgressView, params); + footer.addView(cont, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + + list.setAdapter(adapter); + list.setOnItemClickListener((parent, view, position, id) -> { + Object item = parent.getItemAtPosition(position); + if (item != null && item instanceof GroupMember) { + GroupMember groupMember = (GroupMember) item; + if (groupMember.getUid() != myUid()) { + UserVM userVM = users().get(groupMember.getUid()); + if (userVM != null) { + startActivity(Intents.openPrivateDialog(userVM.getId(), true, getActivity())); + } + } + } + }); + + list.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView adapterView, View view, int i, long l) { + Object item = adapterView.getItemAtPosition(i); + if (item != null && item instanceof GroupMember) { + GroupMember groupMember = (GroupMember) item; + if (groupMember.getUid() != myUid()) { + UserVM userVM = users().get(groupMember.getUid()); + if (userVM != null) { + adapter.onMemberClick(groupVM, userVM, groupMember.isAdministrator(), groupMember.getInviterUid() == myUid(), (BaseActivity) getActivity()); + return true; + } + } + } + return false; + } + }); + + progressView = (CircularProgressBar) res.findViewById(R.id.loadingProgress); + progressView.setIndeterminate(true); + + return res; + } + + @Override + public void onResume() { + super.onResume(); + adapter.initLoad(new MembersAdapter.LoadedCallback() { + @Override + public void onLoaded() { + hideView(progressView); + showView(footer); + } + + @Override + public void onLoadedToEnd() { + hideView(footer); + } + }); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + adapter.dispose(); + } +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/view/MembersAdapter.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/view/MembersAdapter.java index ad03b224e9..0585b67404 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/view/MembersAdapter.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/group/view/MembersAdapter.java @@ -1,16 +1,34 @@ package im.actor.sdk.controllers.group.view; import android.app.Activity; +import android.app.AlertDialog; import android.content.Context; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import android.widget.Toast; +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.i18n.phonenumbers.Phonenumber; + +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Optional; +import im.actor.core.viewmodel.CommandCallback; +import im.actor.core.viewmodel.GroupVM; +import im.actor.core.viewmodel.UserPhone; +import im.actor.runtime.actors.messages.*; +import im.actor.runtime.actors.messages.Void; import im.actor.sdk.ActorSDK; +import im.actor.sdk.ActorSDKLauncher; import im.actor.sdk.R; import im.actor.sdk.controllers.ActorBinder; +import im.actor.sdk.controllers.Intents; +import im.actor.sdk.controllers.activity.BaseActivity; +import im.actor.sdk.util.AlertListBuilder; import im.actor.sdk.util.Screen; import im.actor.sdk.view.avatar.AvatarView; import im.actor.sdk.view.adapters.HolderAdapter; @@ -18,36 +36,113 @@ import im.actor.core.entity.GroupMember; import im.actor.core.viewmodel.UserVM; +import static im.actor.sdk.util.ActorSDKMessenger.messenger; import static im.actor.sdk.util.ActorSDKMessenger.users; public class MembersAdapter extends HolderAdapter { - private GroupMember[] members; - private ActorBinder BINDER = new ActorBinder(); + public static final int LOAD_GAP = 10; + private static final int LIMIT = 20; + private ArrayList members = new ArrayList(); + private ActorBinder BINDER = new ActorBinder(); + private boolean loadInProgress = false; + private boolean loaddedToEnd = false; + private LoadedCallback callback; - public MembersAdapter(Collection members, Context context) { + public MembersAdapter(Context context, int groupId) { super(context); - this.members = members.toArray(new GroupMember[0]); + this.groupId = groupId; + } + + public void setMembers(Collection members) { + setMembers(members, true, true); } - public void updateUid(Collection members) { - this.members = members.toArray(new GroupMember[0]); + public void setMembers(Collection members, boolean clear, boolean sort) { + if (clear) { + this.members.clear(); + } + if (sort) { + GroupMember[] membersArray = members.toArray(new GroupMember[members.size()]); + Arrays.sort(membersArray, (a, b) -> { + if (a.isAdministrator() && !b.isAdministrator()) { + return -1; + } + if (b.isAdministrator() && !a.isAdministrator()) { + return 1; + } + String an = users().get(a.getUid()).getName().get(); + String bn = users().get(b.getUid()).getName().get(); + return an.compareTo(bn); + }); + this.members.addAll(Arrays.asList(membersArray)); + } else { + this.members.addAll(members); + } notifyDataSetChanged(); } + @Override + protected void onBindViewHolder(ViewHolder holder, GroupMember obj, int position, Context context) { + super.onBindViewHolder(holder, obj, position, context); + if (position >= getCount() - LOAD_GAP) { + loadMore(); + } + + } + + private int groupId; + private boolean isInitiallyLoaded; + private byte[] nextMembers; + private ArrayList rawMembers = new ArrayList<>(); + + public void initLoad(LoadedCallback callback) { + this.callback = callback; + if (!isInitiallyLoaded) { + loadMore(); + } + } + + public interface LoadedCallback { + void onLoaded(); + + void onLoadedToEnd(); + } + + private void loadMore() { + if (!loadInProgress && !loaddedToEnd) { + loadInProgress = true; + messenger().loadMembers(groupId, LIMIT, nextMembers).then(groupMembersSlice -> { + if (!isInitiallyLoaded) { + isInitiallyLoaded = true; + if (callback != null) { + callback.onLoaded(); + } + } + nextMembers = groupMembersSlice.getNext(); + loaddedToEnd = nextMembers == null; + if (loaddedToEnd && callback != null) { + callback.onLoadedToEnd(); + } + loadInProgress = false; + setMembers(groupMembersSlice.getMembers(), false, false); + }); + } + } + @Override public int getCount() { - return members.length; + return members.size(); } @Override public GroupMember getItem(int position) { - return members[position]; + return members.get(position); } @Override public long getItemId(int position) { - return members[position].getUid(); + return members.get(position).getUid(); } @Override @@ -74,21 +169,23 @@ public View init(GroupMember data, ViewGroup viewGroup, Context context) { online = (TextView) res.findViewById(R.id.online); ((TextView) admin).setTextColor(ActorSDK.sharedActor().style.getGroupAdminColor()); ((TextView) res.findViewById(R.id.name)).setTextColor(ActorSDK.sharedActor().style.getTextPrimaryColor()); - res.findViewById(R.id.divider).setBackgroundColor(ActorSDK.sharedActor().style.getDividerColor()); + // res.findViewById(R.id.divider).setBackgroundColor(ActorSDK.sharedActor().style.getDividerColor()); return res; } @Override public void bind(GroupMember data, int position, Context context) { + boolean needRebind = user == null || data.getUid() != user.getId(); user = users().get(data.getUid()); ActorSDK.sharedActor().getMessenger().onUserVisible(data.getUid()); onlineBinding = BINDER.bindOnline(online, user); - avatarView.bind(user); + if (needRebind) { + avatarView.bind(user); + } userName.setText(user.getName().get()); - if (data.isAdministrator()) { admin.setVisibility(View.VISIBLE); } else { @@ -113,4 +210,90 @@ public void dispose() { super.dispose(); BINDER.unbindAll(); } + + public void onMemberClick(GroupVM groupVM, UserVM userVM, boolean isAdministrator, boolean isInvitedByMe, BaseActivity activity) { + AlertListBuilder alertListBuilder = new AlertListBuilder(); + final ArrayList phones = userVM.getPhones().get(); + alertListBuilder.addItem(activity.getString(R.string.group_context_message).replace("{0}", userVM.getName().get()), () -> activity.startActivity(Intents.openPrivateDialog(userVM.getId(), true, activity))); + if (phones.size() != 0) { + alertListBuilder.addItem(activity.getString(R.string.group_context_call).replace("{0}", userVM.getName().get()), () -> { + if (phones.size() == 1) { + activity.startActivity(Intents.call(phones.get(0).getPhone())); + } else { + CharSequence[] sequences = new CharSequence[phones.size()]; + for (int i = 0; i < sequences.length; i++) { + try { + Phonenumber.PhoneNumber number = PhoneNumberUtil.getInstance().parse("+" + phones.get(i).getPhone(), "us"); + sequences[i] = phones.get(i).getTitle() + ": " + PhoneNumberUtil.getInstance().format(number, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL); + } catch (NumberParseException e) { + e.printStackTrace(); + sequences[i] = phones.get(i).getTitle() + ": +" + phones.get(i).getPhone(); + } + } + new AlertDialog.Builder(activity) + .setItems(sequences, (dialog1, which1) -> { + activity.startActivity(Intents.call(phones.get(which1).getPhone())); + }) + .show() + .setCanceledOnTouchOutside(true); + } + }); + } + alertListBuilder.addItem(activity.getString(R.string.group_context_view).replace("{0}", userVM.getName().get()), () -> ActorSDKLauncher.startProfileActivity(activity, userVM.getId())); + if (groupVM.getIsCanKickAnyone().get() || (groupVM.getIsCanKickInvited().get() && isInvitedByMe)) { + alertListBuilder.addItem(activity.getString(R.string.group_context_remove).replace("{0}", userVM.getName().get()), () -> { + new AlertDialog.Builder(activity) + .setMessage(activity.getString(R.string.alert_group_remove_text).replace("{0}", userVM.getName().get())) + .setPositiveButton(R.string.alert_group_remove_yes, (dialog2, which1) -> { + activity.execute(messenger().kickMember(groupVM.getId(), userVM.getId()), + R.string.progress_common, new CommandCallback() { + @Override + public void onResult(Void res1) { + + } + + @Override + public void onError(Exception e) { + Toast.makeText(activity, R.string.toast_unable_kick, Toast.LENGTH_SHORT).show(); + } + }); + }) + .setNegativeButton(R.string.dialog_cancel, null) + .show() + .setCanceledOnTouchOutside(true); + }); + } + if (groupVM.getIsCanEditAdmins().get() && !userVM.isBot()) { + alertListBuilder.addItem(!isAdministrator ? activity.getResources().getString(R.string.group_make_admin) : activity.getResources().getString(R.string.group_revoke_admin), () -> { + if (!isAdministrator) { + messenger().makeAdmin(groupVM.getId(), userVM.getId()).start(new CommandCallback() { + @Override + public void onResult(Void res) { + + } + + @Override + public void onError(Exception e) { + + } + }); + } else { + messenger().revokeAdmin(groupVM.getId(), userVM.getId()).start(new CommandCallback() { + @Override + public void onResult(Void res) { + + } + + @Override + public void onError(Exception e) { + + } + }); + } + }); + } + alertListBuilder.build(activity) + .show() + .setCanceledOnTouchOutside(true); + } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/profile/ProfileActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/profile/ProfileActivity.java index dda21135a7..25527bf018 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/profile/ProfileActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/profile/ProfileActivity.java @@ -44,7 +44,7 @@ protected void onCreate(Bundle savedInstanceState) { if (fragment == null) { fragment = ProfileFragment.create(uid); } - showFragment(fragment, false, false); + showFragment(fragment, false); } } } \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/profile/ProfileFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/profile/ProfileFragment.java index cba7c1f39c..97201e3b59 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/profile/ProfileFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/profile/ProfileFragment.java @@ -1,20 +1,27 @@ package im.actor.sdk.controllers.profile; +import android.app.Activity; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; +import android.content.res.ColorStateList; import android.graphics.Color; +import android.graphics.PorterDuff; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.media.RingtoneManager; import android.net.Uri; import android.os.Bundle; +import android.provider.Settings; +import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.SwitchCompat; +import android.util.StateSet; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -32,26 +39,31 @@ import java.util.ArrayList; +import im.actor.core.entity.Peer; import im.actor.core.viewmodel.UserEmail; +import im.actor.core.viewmodel.UserPhone; +import im.actor.core.viewmodel.UserVM; import im.actor.runtime.mvvm.Value; import im.actor.runtime.mvvm.ValueChangedListener; import im.actor.sdk.ActorSDK; import im.actor.sdk.R; +import im.actor.sdk.controllers.BaseFragment; import im.actor.sdk.controllers.Intents; +import im.actor.sdk.controllers.compose.ComposeActivity; import im.actor.sdk.controllers.fragment.preview.ViewAvatarActivity; -import im.actor.sdk.controllers.BaseFragment; import im.actor.sdk.util.Screen; +import im.actor.sdk.util.ViewUtils; import im.actor.sdk.view.avatar.AvatarView; -import im.actor.core.entity.Peer; -import im.actor.core.viewmodel.UserPhone; -import im.actor.core.viewmodel.UserVM; import static im.actor.sdk.util.ActorSDKMessenger.messenger; import static im.actor.sdk.util.ActorSDKMessenger.users; public class ProfileFragment extends BaseFragment { + public static int SOUND_PICKER_REQUEST_CODE = 122; + public static final String EXTRA_UID = "uid"; + private View recordFieldWithIcon; public static ProfileFragment create(int uid) { Bundle bundle = new Bundle(); @@ -103,7 +115,7 @@ public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bun // avatarView = (AvatarView) res.findViewById(R.id.avatar); - avatarView.init(Screen.dp(96), 44); + avatarView.init(Screen.dp(48), 22); avatarView.bind(user.getAvatar().get(), user.getName().get(), user.getId()); avatarView.setOnClickListener(v -> { startActivity(ViewAvatarActivity.viewAvatar(user.getId(), getActivity())); @@ -127,33 +139,48 @@ public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bun lastSeen.setTextColor(style.getProfileSubtitleColor()); bind(lastSeen, user); + // + // Fab + // + + FloatingActionButton fab = (FloatingActionButton) res.findViewById(R.id.fab); + + fab.setBackgroundTintList(new ColorStateList(new int[][]{ + new int[]{android.R.attr.state_pressed}, + StateSet.WILD_CARD, + + }, new int[]{ + ActorSDK.sharedActor().style.getFabPressedColor(), + ActorSDK.sharedActor().style.getFabColor(), + })); + fab.setRippleColor(ActorSDK.sharedActor().style.getFabPressedColor()); + fab.setOnClickListener(v -> startActivity(new Intent(getActivity(), ComposeActivity.class))); // - // Add/Remove Contact + // Remove Contact // - final View addContact = res.findViewById(R.id.addContact); - final ImageView addContactIcon = (ImageView) addContact.findViewById(R.id.addContactIcon); - final TextView addContactTitle = (TextView) addContact.findViewById(R.id.addContactTitle); + final View removeContact = res.findViewById(R.id.addContact); + final TextView addContactTitle = (TextView) removeContact.findViewById(R.id.addContactTitle); + addContactTitle.setText(getString(R.string.profile_contacts_added)); + addContactTitle.setTextColor(style.getTextPrimaryColor()); + removeContact.setOnClickListener(v -> { + execute(ActorSDK.sharedActor().getMessenger().removeContact(user.getId())); + }); + bind(user.isContact(), (isContact, valueModel) -> { if (isContact) { - addContactTitle.setText(getString(R.string.profile_contacts_added)); - addContactTitle.setTextColor(style.getProfileContactIconColor()); - Drawable drawable = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_check_circle_black_24dp)); - DrawableCompat.setTint(drawable, style.getProfileContactIconColor()); - addContactIcon.setImageDrawable(drawable); - addContact.setOnClickListener(v -> { - execute(ActorSDK.sharedActor().getMessenger().removeContact(user.getId())); - }); + removeContact.setVisibility(View.VISIBLE); + + //fab + fab.setImageResource(R.drawable.ic_message_white_24dp); + fab.setOnClickListener(view -> startActivity(Intents.openPrivateDialog(user.getId(), true, getActivity()))); } else { - addContactTitle.setText(getString(R.string.profile_contacts_available)); - addContactTitle.setTextColor(style.getProfileContactIconColor()); - Drawable drawable = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_person_add_white_24dp)); - DrawableCompat.setTint(drawable, style.getProfileContactIconColor()); - addContactIcon.setImageDrawable(drawable); - addContact.setOnClickListener(v -> { - execute(ActorSDK.sharedActor().getMessenger().addContact(user.getId())); - }); + removeContact.setVisibility(View.GONE); + + //fab + fab.setImageResource(R.drawable.ic_person_add_white_24dp); + fab.setOnClickListener(view -> execute(ActorSDK.sharedActor().getMessenger().addContact(user.getId()))); } }); @@ -166,10 +193,10 @@ public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bun ImageView newMessageIcon = (ImageView) newMessageView.findViewById(R.id.newMessageIcon); TextView newMessageTitle = (TextView) newMessageView.findViewById(R.id.newMessageText); { - Drawable drawable = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_chat_black_24dp)); - DrawableCompat.setTint(drawable, style.getListActionColor()); + Drawable drawable = getResources().getDrawable(R.drawable.ic_chat_black_24dp); + drawable.mutate().setColorFilter(style.getSettingsIconColor(), PorterDuff.Mode.SRC_IN); newMessageIcon.setImageDrawable(drawable); - newMessageTitle.setTextColor(style.getListActionColor()); + newMessageTitle.setTextColor(style.getTextPrimaryColor()); } newMessageView.setOnClickListener(v -> { startActivity(Intents.openPrivateDialog(user.getId(), true, getActivity())); @@ -180,29 +207,45 @@ public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bun // Voice Call // + View voiceCallDivider = res.findViewById(R.id.voiceCallDivider); View voiceCallView = res.findViewById(R.id.voiceCall); - if (ActorSDK.sharedActor().isCallsEnabled()) { + if (ActorSDK.sharedActor().isCallsEnabled() && !user.isBot()) { ImageView voiceViewIcon = (ImageView) voiceCallView.findViewById(R.id.actionIcon); TextView voiceViewTitle = (TextView) voiceCallView.findViewById(R.id.actionText); - if (!user.isBot()) { - Drawable drawable = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_phone_white_24dp)); - drawable = drawable.mutate(); - DrawableCompat.setTint(drawable, style.getListActionColor()); - voiceViewIcon.setImageDrawable(drawable); - voiceViewTitle.setTextColor(style.getListActionColor()); - - voiceCallView.setOnClickListener(v -> { - execute(ActorSDK.sharedActor().getMessenger().doCall(user.getId())); - }); - } else { - Drawable drawable = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_phone_white_24dp)); - drawable = drawable.mutate(); - DrawableCompat.setTint(drawable, style.getTextHintColor()); - voiceViewIcon.setImageDrawable(drawable); - voiceViewTitle.setTextColor(style.getTextHintColor()); - } + Drawable drawable = getResources().getDrawable(R.drawable.ic_phone_white_24dp); + drawable.mutate().setColorFilter(style.getSettingsIconColor(), PorterDuff.Mode.SRC_IN); + voiceViewIcon.setImageDrawable(drawable); + voiceViewTitle.setTextColor(style.getTextPrimaryColor()); + + voiceCallView.setOnClickListener(v -> { + execute(ActorSDK.sharedActor().getMessenger().doCall(user.getId())); + }); } else { voiceCallView.setVisibility(View.GONE); + voiceCallDivider.setVisibility(View.GONE); + } + + // + // Video Call + // + + + View videoCallDivider = res.findViewById(R.id.videoCallDivider); + View videoCallView = res.findViewById(R.id.videoCall); + if (ActorSDK.sharedActor().isCallsEnabled() && !user.isBot()) { + ImageView voiceViewIcon = (ImageView) videoCallView.findViewById(R.id.videoCallIcon); + TextView voiceViewTitle = (TextView) videoCallView.findViewById(R.id.videoCallText); + Drawable drawable = getResources().getDrawable(R.drawable.ic_videocam_white_24dp); + drawable.mutate().setColorFilter(style.getSettingsIconColor(), PorterDuff.Mode.SRC_IN); + voiceViewIcon.setImageDrawable(drawable); + voiceViewTitle.setTextColor(style.getTextPrimaryColor()); + + videoCallView.setOnClickListener(v -> { + execute(ActorSDK.sharedActor().getMessenger().doVideoCall(user.getId())); + }); + } else { + videoCallView.setVisibility(View.GONE); + videoCallDivider.setVisibility(View.GONE); } @@ -211,135 +254,172 @@ public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bun // final LinearLayout contactsContainer = (LinearLayout) res.findViewById(R.id.contactsContainer); - boolean isFirstContact = true; + + String aboutString = user.getAbout().get(); + boolean isFirstContact = aboutString == null || aboutString.isEmpty(); // - // Phones + // About // - for (int i = 0; i < phones.size(); i++) { - final UserPhone userPhone = phones.get(i); - - // Formatting Phone Number - String _phoneNumber; - try { - Phonenumber.PhoneNumber number = PhoneNumberUtil.getInstance().parse("+" + userPhone.getPhone(), "us"); - _phoneNumber = PhoneNumberUtil.getInstance().format(number, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL); - } catch (NumberParseException e) { - e.printStackTrace(); - _phoneNumber = "+" + userPhone.getPhone(); + bind(user.getAbout(), new ValueChangedListener() { + private View userAboutRecord; + + @Override + public void onChanged(final String newUserAbout, Value valueModel) { + if (newUserAbout != null && newUserAbout.length() > 0) { + if (userAboutRecord == null) { + userAboutRecord = buildRecordBig(newUserAbout, + R.drawable.ic_info_outline_black_24dp, + true, + false, + inflater, contactsContainer); + } else { + ((TextView) userAboutRecord.findViewById(R.id.value)).setText(newUserAbout); + } + if (recordFieldWithIcon != null) { + recordFieldWithIcon.findViewById(R.id.recordIcon).setVisibility(View.INVISIBLE); + } + } } - final String phoneNumber = _phoneNumber; + }); - String phoneTitle = userPhone.getTitle(); + if (!ActorSDK.sharedActor().isOnClientPrivacyEnabled() || user.isInPhoneBook().get()) { - // "Mobile phone" is default value for non specified title - // Trying to localize this - if (phoneTitle.toLowerCase().equals("mobile phone")) { - phoneTitle = getString(R.string.settings_mobile_phone); - } + // + // Phones + // + for (int i = 0; i < phones.size(); i++) { + final UserPhone userPhone = phones.get(i); + + // Formatting Phone Number + String _phoneNumber; + try { + Phonenumber.PhoneNumber number = PhoneNumberUtil.getInstance().parse("+" + userPhone.getPhone(), "us"); + _phoneNumber = PhoneNumberUtil.getInstance().format(number, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL); + } catch (NumberParseException e) { + e.printStackTrace(); + _phoneNumber = "+" + userPhone.getPhone(); + } + final String phoneNumber = _phoneNumber; - View view = buildRecord(phoneTitle, - phoneNumber, - R.drawable.ic_import_contacts_black_24dp, - isFirstContact, - emails.size() == 0 && i == phones.size() - 1, - inflater, contactsContainer); - - - view.setOnClickListener(v -> { - new AlertDialog.Builder(getActivity()) - .setItems(new CharSequence[]{ - getString(R.string.phone_menu_call).replace("{0}", phoneNumber), - getString(R.string.phone_menu_sms).replace("{0}", phoneNumber), - getString(R.string.phone_menu_share).replace("{0}", phoneNumber), - getString(R.string.phone_menu_copy) - }, (dialog, which) -> { - if (which == 0) { - startActivity(new Intent(Intent.ACTION_DIAL) - .setData(Uri.parse("tel:+" + userPhone.getPhone()))); - } else if (which == 1) { - startActivity(new Intent(Intent.ACTION_VIEW) - .setData(Uri.parse("sms:+" + userPhone.getPhone()))); - } else if (which == 2) { - startActivity(new Intent(Intent.ACTION_SEND) - .setType("text/plain") - .putExtra(Intent.EXTRA_TEXT, getString(R.string.settings_share_text) - .replace("{0}", phoneNumber) - .replace("{1}", user.getName().get()))); - } else if (which == 3) { - ClipboardManager clipboard = - (ClipboardManager) getActivity() - .getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("Phone number", phoneNumber); - clipboard.setPrimaryClip(clip); - Snackbar.make(res, R.string.toast_phone_copied, Snackbar.LENGTH_SHORT) - .show(); - } - }) - .show() - .setCanceledOnTouchOutside(true); - }); - view.setOnLongClickListener(v -> { - ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("Phone number", "+" + userPhone.getPhone()); - clipboard.setPrimaryClip(clip); - Snackbar.make(res, R.string.toast_phone_copied, Snackbar.LENGTH_SHORT) - .show(); - return true; - }); + String phoneTitle = userPhone.getTitle(); - isFirstContact = false; - } + // "Mobile phone" is default value for non specified title + // Trying to localize this + if (phoneTitle.toLowerCase().equals("mobile phone")) { + phoneTitle = getString(R.string.settings_mobile_phone); + } - // - // Emails - // + View view = buildRecord(phoneTitle, + phoneNumber, + R.drawable.ic_import_contacts_black_24dp, + isFirstContact, + false, + inflater, contactsContainer); + if (isFirstContact) { + recordFieldWithIcon = view; + } - for (int i = 0; i < emails.size(); i++) { - final UserEmail userEmail = emails.get(i); - View view = buildRecord(userEmail.getTitle(), - userEmail.getEmail(), - R.drawable.ic_import_contacts_black_24dp, - isFirstContact, - userName == null && i == emails.size() - 1, - inflater, contactsContainer); - - view.setOnClickListener(v -> { - new AlertDialog.Builder(getActivity()) - .setItems(new CharSequence[]{ - getString(R.string.email_menu_email).replace("{0}", userEmail.getEmail()), - getString(R.string.phone_menu_copy) - }, (dialog, which) -> { - if (which == 0) { - startActivity(new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", userEmail.getEmail(), null))); - } else if (which == 1) { - ClipboardManager clipboard = - (ClipboardManager) getActivity() - .getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("Email", userEmail.getEmail()); - clipboard.setPrimaryClip(clip); - Snackbar.make(res, R.string.toast_email_copied, Snackbar.LENGTH_SHORT) - .show(); - } - }) - .show() - .setCanceledOnTouchOutside(true); - }); - view.setOnLongClickListener(v -> { - ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("Email", "+" + userEmail.getEmail()); - clipboard.setPrimaryClip(clip); - Snackbar.make(res, R.string.toast_email_copied, Snackbar.LENGTH_SHORT) - .show(); - return true; - }); - isFirstContact = false; + view.setOnClickListener(v -> { + new AlertDialog.Builder(getActivity()) + .setItems(new CharSequence[]{ + getString(R.string.phone_menu_call).replace("{0}", phoneNumber), + getString(R.string.phone_menu_sms).replace("{0}", phoneNumber), + getString(R.string.phone_menu_share).replace("{0}", phoneNumber), + getString(R.string.phone_menu_copy) + }, (dialog, which) -> { + if (which == 0) { + startActivity(new Intent(Intent.ACTION_DIAL) + .setData(Uri.parse("tel:+" + userPhone.getPhone()))); + } else if (which == 1) { + startActivity(new Intent(Intent.ACTION_VIEW) + .setData(Uri.parse("sms:+" + userPhone.getPhone()))); + } else if (which == 2) { + startActivity(new Intent(Intent.ACTION_SEND) + .setType("text/plain") + .putExtra(Intent.EXTRA_TEXT, getString(R.string.settings_share_text) + .replace("{0}", phoneNumber) + .replace("{1}", user.getName().get()))); + } else if (which == 3) { + ClipboardManager clipboard = + (ClipboardManager) getActivity() + .getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Phone number", phoneNumber); + clipboard.setPrimaryClip(clip); + Snackbar.make(res, R.string.toast_phone_copied, Snackbar.LENGTH_SHORT) + .show(); + } + }) + .show() + .setCanceledOnTouchOutside(true); + }); + + view.setOnLongClickListener(v -> { + ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Phone number", "+" + userPhone.getPhone()); + clipboard.setPrimaryClip(clip); + Snackbar.make(res, R.string.toast_phone_copied, Snackbar.LENGTH_SHORT) + .show(); + return true; + }); + + isFirstContact = false; + } + + // + // Emails + // + + for (int i = 0; i < emails.size(); i++) { + final UserEmail userEmail = emails.get(i); + View view = buildRecord(userEmail.getTitle(), + userEmail.getEmail(), + R.drawable.ic_import_contacts_black_24dp, + isFirstContact, + false, + inflater, contactsContainer); + if (isFirstContact) { + recordFieldWithIcon = view; + } + + view.setOnClickListener(v -> { + new AlertDialog.Builder(getActivity()) + .setItems(new CharSequence[]{ + getString(R.string.email_menu_email).replace("{0}", userEmail.getEmail()), + getString(R.string.phone_menu_copy) + }, (dialog, which) -> { + if (which == 0) { + startActivity(new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", userEmail.getEmail(), null))); + } else if (which == 1) { + ClipboardManager clipboard = + (ClipboardManager) getActivity() + .getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Email", userEmail.getEmail()); + clipboard.setPrimaryClip(clip); + Snackbar.make(res, R.string.toast_email_copied, Snackbar.LENGTH_SHORT) + .show(); + } + }) + .show() + .setCanceledOnTouchOutside(true); + }); + view.setOnLongClickListener(v -> { + ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Email", "+" + userEmail.getEmail()); + clipboard.setPrimaryClip(clip); + Snackbar.make(res, R.string.toast_email_copied, Snackbar.LENGTH_SHORT) + .show(); + return true; + }); + isFirstContact = false; + } } + // // Username // @@ -354,7 +434,7 @@ public void onChanged(final String newUserName, Value valueModel) { userNameRecord = buildRecord(getString(R.string.nickname), "@" + newUserName, R.drawable.ic_import_contacts_black_24dp, finalIsFirstContact, - true, + false, inflater, contactsContainer); } else { ((TextView) userNameRecord.findViewById(R.id.value)).setText(newUserName); @@ -369,28 +449,9 @@ public void onChanged(final String newUserName, Value valueModel) { .show(); return true; }); - } - } - }); - - // - // About - // - - bind(user.getAbout(), new ValueChangedListener() { - private View userAboutRecord; - @Override - public void onChanged(final String newUserAbout, Value valueModel) { - if (newUserAbout != null && newUserAbout.length() > 0) { - if (userAboutRecord == null) { - userAboutRecord = buildRecordBig(newUserAbout, - R.drawable.ic_info_outline_black_24dp, - true, - true, - inflater, contactsContainer); - } else { - ((TextView) userAboutRecord.findViewById(R.id.value)).setText(newUserAbout); + if (finalIsFirstContact) { + recordFieldWithIcon = userNameRecord; } } } @@ -405,16 +466,62 @@ public void onChanged(final String newUserAbout, Value valueModel) { // Notifications // View notificationContainer = res.findViewById(R.id.notificationsCont); + View notificationPickerContainer = res.findViewById(R.id.notificationsPickerCont); + ((TextView) notificationContainer.findViewById(R.id.settings_notifications_title)).setTextColor(style.getTextPrimaryColor()); final SwitchCompat notificationEnable = (SwitchCompat) res.findViewById(R.id.enableNotifications); - notificationEnable.setChecked(messenger().isNotificationsEnabled(Peer.user(user.getId()))); - notificationEnable.setOnCheckedChangeListener((buttonView, isChecked) -> messenger().changeNotificationsEnabled(Peer.user(user.getId()), isChecked)); + Peer peer = Peer.user(user.getId()); + notificationEnable.setChecked(messenger().isNotificationsEnabled(peer)); + if (messenger().isNotificationsEnabled(peer)) { + ViewUtils.showView(notificationPickerContainer, false); + } else { + ViewUtils.goneView(notificationPickerContainer, false); + } + notificationEnable.setOnCheckedChangeListener((buttonView, isChecked) -> { + messenger().changeNotificationsEnabled(Peer.user(user.getId()), isChecked); + + if (isChecked) { + ViewUtils.showView(notificationPickerContainer, false); + } else { + ViewUtils.goneView(notificationPickerContainer, false); + } + }); notificationContainer.setOnClickListener(v -> notificationEnable.setChecked(!notificationEnable.isChecked())); ImageView iconView = (ImageView) res.findViewById(R.id.settings_notification_icon); Drawable drawable = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_list_black_24dp)); + drawable.mutate(); DrawableCompat.setTint(drawable, style.getSettingsIconColor()); iconView.setImageDrawable(drawable); + ((TextView) notificationPickerContainer.findViewById(R.id.settings_notifications_picker_title)).setTextColor(style.getTextPrimaryColor()); + notificationPickerContainer.setOnClickListener(view -> { + Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)); + + Uri currentSound = null; + String defaultPath = null; + Uri defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI; + if (defaultUri != null) { + defaultPath = defaultUri.getPath(); + } + + String path = messenger().getPreferences().getString("userNotificationSound_" + uid); + if (path == null) { + path = defaultPath; + } + if (path != null && !path.equals("none")) { + if (path.equals(defaultPath)) { + currentSound = defaultUri; + } else { + currentSound = Uri.parse(path); + } + } + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, currentSound); + startActivityForResult(intent, SOUND_PICKER_REQUEST_CODE); + }); + // // Block // @@ -443,6 +550,7 @@ public void onChanged(final String newUserAbout, Value valueModel) { }); ImageView blockIconView = (ImageView) res.findViewById(R.id.settings_block_icon); Drawable blockDrawable = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_block_white_24dp)); + drawable.mutate(); DrawableCompat.setTint(blockDrawable, style.getSettingsIconColor()); blockIconView.setImageDrawable(blockDrawable); } @@ -458,6 +566,23 @@ public void onChanged(final String newUserAbout, Value valueModel) { return res; } + private void checkInfiIcon() { + + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK && requestCode == SOUND_PICKER_REQUEST_CODE) { + Uri ringtone = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); + + if (ringtone != null) { + messenger().getPreferences().putString("userNotificationSound_" + uid, ringtone.toString()); + } else { + messenger().getPreferences().putString("userNotificationSound_" + uid, "none"); + } + } + } + @Override public void onResume() { super.onResume(); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/root/RootActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/root/RootActivity.java index f71aa04f51..1afc353039 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/root/RootActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/root/RootActivity.java @@ -1,24 +1,39 @@ package im.actor.sdk.controllers.root; +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; +import android.support.v13.app.ActivityCompat; import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; import android.support.v7.widget.Toolbar; +import im.actor.core.viewmodel.AppStateVM; import im.actor.sdk.ActorSDK; import im.actor.sdk.R; import im.actor.sdk.controllers.activity.BaseFragmentActivity; +import im.actor.sdk.controllers.tools.InviteHandler; /** * Root Activity of Application */ public class RootActivity extends BaseFragmentActivity { + private static final int PERMISSIONS_REQUEST_READ_CONTACTS = 1; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_root); + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.READ_CONTACTS}, + PERMISSIONS_REQUEST_READ_CONTACTS); + } + // // Configure Toolbar // @@ -38,5 +53,33 @@ protected void onCreate(Bundle savedInstanceState) { .add(R.id.root, fragment) .commit(); } + + InviteHandler.handleIntent(this, getIntent()); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + InviteHandler.handleIntent(this, intent); + } + + @Override + public void onRequestPermissionsResult(int requestCode, + String permissions[], int[] grantResults) { + switch (requestCode) { + case PERMISSIONS_REQUEST_READ_CONTACTS: { + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + + AppStateVM appStateVM = ActorSDK.sharedActor().getMessenger().getAppStateVM(); + if (appStateVM.isDialogsLoaded() && appStateVM.isContactsLoaded() && appStateVM.isSettingsLoaded()) { + ActorSDK.sharedActor().getMessenger().startImport(); + } + + } + } + + } } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/root/RootFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/root/RootFragment.java index ed1057c71b..382abab932 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/root/RootFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/root/RootFragment.java @@ -9,6 +9,7 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.FrameLayout; import im.actor.sdk.ActorSDK; import im.actor.sdk.R; @@ -28,22 +29,59 @@ public RootFragment() { setTitle(ActorSDK.sharedActor().getAppName()); } + private boolean isInited = false; + + @Override + public void onCreate(Bundle saveInstance) { + super.onCreate(saveInstance); + if (saveInstance != null) { + isInited = saveInstance.getBoolean("is_inited"); + } + } + @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View res = inflater.inflate(R.layout.activity_root_content, container, false); + FrameLayout res = new FrameLayout(getContext()); - if (savedInstanceState == null) { + FrameLayout content = new FrameLayout(getContext()); + content.setId(R.id.content); + res.addView(content, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + FrameLayout fab = new FrameLayout(getContext()); + fab.setId(R.id.fab); + res.addView(fab, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + FrameLayout search = new FrameLayout(getContext()); + search.setId(R.id.search); + res.addView(search, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + FrameLayout placeholder = new FrameLayout(getContext()); + placeholder.setId(R.id.placeholder); + res.addView(placeholder, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + return res; + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + if (!isInited) { + isInited = true; + DialogsDefaultFragment dialogsDefaultFragment = ActorSDK.sharedActor().getDelegate().fragmentForDialogs(); getChildFragmentManager().beginTransaction() - .add(R.id.content, new DialogsDefaultFragment()) + .add(R.id.content, dialogsDefaultFragment != null ? dialogsDefaultFragment : new DialogsDefaultFragment()) .add(R.id.fab, new ComposeFabFragment()) .add(R.id.search, new GlobalSearchDefaultFragment()) .add(R.id.placeholder, new GlobalPlaceholderFragment()) .commit(); } - - return res; } @Override @@ -68,4 +106,10 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean("is_inited", isInited); + } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/search/GlobalSearchBaseFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/search/GlobalSearchBaseFragment.java index dec22056b0..b2c7186bd8 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/search/GlobalSearchBaseFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/search/GlobalSearchBaseFragment.java @@ -8,6 +8,7 @@ import android.support.v7.widget.ChatLinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.SearchView; +import android.support.v7.widget.SimpleItemAnimator; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -15,24 +16,32 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; -import android.widget.LinearLayout; import android.widget.TextView; +import java.util.ArrayList; +import java.util.List; + +import im.actor.core.entity.Avatar; import im.actor.core.entity.Peer; +import im.actor.core.entity.PeerSearchEntity; +import im.actor.core.entity.PeerType; import im.actor.core.entity.SearchEntity; import im.actor.core.viewmodel.CommandCallback; +import im.actor.core.viewmodel.GroupVM; import im.actor.core.viewmodel.UserVM; import im.actor.runtime.generic.mvvm.BindedDisplayList; import im.actor.runtime.generic.mvvm.DisplayList; +import im.actor.runtime.generic.mvvm.alg.Modifications; import im.actor.sdk.ActorSDK; import im.actor.sdk.R; import im.actor.sdk.controllers.BaseFragment; -import im.actor.sdk.controllers.Intents; import im.actor.sdk.util.Screen; import im.actor.sdk.view.adapters.HeaderViewRecyclerAdapter; import im.actor.sdk.view.adapters.OnItemClickedListener; +import static im.actor.sdk.util.ActorSDKMessenger.groups; import static im.actor.sdk.util.ActorSDKMessenger.messenger; +import static im.actor.sdk.util.ActorSDKMessenger.users; public abstract class GlobalSearchBaseFragment extends BaseFragment { @@ -49,9 +58,9 @@ public abstract class GlobalSearchBaseFragment extends BaseFragment { private BindedDisplayList searchDisplay; private final DisplayList.Listener searchListener = () -> onSearchChanged(); - SearchHolder footerSearchHolder; private String searchQuery; - private LinearLayout footer; + private boolean scrolledToEnd = true; + private ArrayList globalSearchResults = new ArrayList<>(); public GlobalSearchBaseFragment() { setHasOptionsMenu(true); @@ -151,33 +160,47 @@ public boolean onQueryTextChange(String s) { if (s.trim().length() > 0) { String activeSearchQuery = searchQuery; searchDisplay.initSearch(s.trim().toLowerCase(), false); + scrolledToEnd = false; searchAdapter.setQuery(s.trim().toLowerCase()); - messenger().findUsers(s).start(new CommandCallback() { + globalSearchResults.clear(); + messenger().findPeers(s).start(new CommandCallback>() { @Override - public void onResult(UserVM[] res) { - int footerVisability = footer.getVisibility(); + public void onResult(List res) { if (searchQuery.equals(activeSearchQuery)) { - boolean showResult = false; - UserVM u = null; - if (res.length > 0) { - u = res[0]; - showResult = true; + int order = 0; + outer: + for (PeerSearchEntity pse : res) { for (int i = 0; i < searchDisplay.getSize(); i++) { - if (searchDisplay.getItem(i).getPeer().equals(Peer.user(u.getId()))) - showResult = false; - break; + if (searchDisplay.getItem(i).getPeer().equals(pse.getPeer())) { + continue outer; + } + } + + Avatar avatar; + Peer peer = pse.getPeer(); + String name; + if (peer.getPeerType() == PeerType.PRIVATE) { + UserVM userVM = users().get(peer.getPeerId()); + name = userVM.getName().get(); + avatar = userVM.getAvatar().get(); + } else if (peer.getPeerType() == PeerType.GROUP) { + GroupVM groupVM = groups().get(peer.getPeerId()); + name = groupVM.getName().get(); + avatar = groupVM.getAvatar().get(); + } else { + continue; } + String optMatchString = pse.getOptMatchString(); + globalSearchResults.add(new SearchEntity(pse.getPeer(), order++, avatar, optMatchString == null ? name : optMatchString)); } - if (showResult) { - footerSearchHolder.bind(new SearchEntity(Peer.user(u.getId()), 0, u.getAvatar().get(), u.getName().get()), activeSearchQuery, true); - showView(footer); - } else { - goneView(footer); + if (globalSearchResults.size() > 0) { + globalSearchResults.add(new SearchEntityHeader(order++)); } - } - if (footerVisability != footer.getVisibility()) { + checkGlobalSearch(); onSearchChanged(); + } + } @Override @@ -185,10 +208,9 @@ public void onError(Exception e) { } }); + } else { searchDisplay.initEmpty(); - goneView(footer); - } } return false; @@ -197,12 +219,15 @@ public void onError(Exception e) { } private void onSearchChanged() { + if (searchDisplay == null) { + return; + } if (!searchDisplay.isInSearchState()) { showView(searchHintView); goneView(searchEmptyView); } else { goneView(searchHintView); - if (searchDisplay.getSize() == 0 && footer.getVisibility() != View.VISIBLE) { + if (searchDisplay.getSize() == 0) { showView(searchEmptyView); } else { goneView(searchEmptyView); @@ -217,6 +242,18 @@ private void showSearch() { isSearchVisible = true; searchDisplay = messenger().buildSearchDisplayList(); + searchDisplay.setBindHook(new BindedDisplayList.BindHook() { + @Override + public void onScrolledToEnd() { + scrolledToEnd = true; + checkGlobalSearch(); + } + + @Override + public void onItemTouched(SearchEntity item) { + + } + }); searchAdapter = new SearchAdapter(getActivity(), searchDisplay, new OnItemClickedListener() { @Override public void onClicked(SearchEntity item) { @@ -236,52 +273,15 @@ public boolean onLongClicked(SearchEntity item) { header.setBackgroundColor(ActorSDK.sharedActor().style.getMainBackgroundColor()); recyclerAdapter.addHeaderView(header); - TextView footerTitle = new TextView(getActivity()); - footerTitle.setText(R.string.main_search_global_header); - footerTitle.setTextSize(16); - footerTitle.setPadding(Screen.dp(12), Screen.dp(8), 0, Screen.dp(8)); - footerTitle.setBackgroundColor(ActorSDK.sharedActor().style.getBackyardBackgroundColor()); - footerTitle.setTextColor(ActorSDK.sharedActor().style.getTextSecondaryColor()); - - footerSearchHolder = new SearchHolder(getActivity(), new OnItemClickedListener() { - @Override - public void onClicked(SearchEntity item) { - int peerId = item.getPeer().getPeerId(); - execute(messenger().addContact(peerId), R.string.progress_common, new CommandCallback() { - @Override - public void onResult(Boolean res2) { - startActivity(Intents.openPrivateDialog(peerId, - true, - getActivity())); - } - - @Override - public void onError(Exception e) { - startActivity(Intents.openPrivateDialog(peerId, - true, - getActivity())); - } - }); - } - - @Override - public boolean onLongClicked(SearchEntity item) { - return false; - } - }); - View footerGlobalSearchView = footerSearchHolder.itemView; + searchList.setAdapter(recyclerAdapter); - footer = new LinearLayout(getActivity()); - footer.setOrientation(LinearLayout.VERTICAL); - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(Screen.getWidth(), ViewGroup.LayoutParams.WRAP_CONTENT); - footer.addView(footerTitle, params); - footer.addView(footerGlobalSearchView, params); + RecyclerView.ItemAnimator animator = searchList.getItemAnimator(); - footer.setVisibility(View.GONE); + if (animator instanceof SimpleItemAnimator) { + ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false); + } - recyclerAdapter.addFooterView(footer); - searchList.setAdapter(recyclerAdapter); searchDisplay.addListener(searchListener); showView(searchHintView, false); goneView(searchEmptyView, false); @@ -294,6 +294,12 @@ public boolean onLongClicked(SearchEntity item) { } } + private void checkGlobalSearch() { + if ((scrolledToEnd || searchDisplay.getSize() == 0) && globalSearchResults.size() > 0) { + searchDisplay.editList(Modifications.addLoadMore(globalSearchResults)); + } + } + private void hideSearch() { if (!isSearchVisible) { return; @@ -306,6 +312,7 @@ private void hideSearch() { } searchAdapter = null; searchList.setAdapter(null); + searchQuery = null; goneView(searchContainer, false); if (searchMenu != null) { @@ -320,5 +327,20 @@ private void hideSearch() { } } + public class SearchEntityHeader extends SearchEntity { + + public SearchEntityHeader(int order) { + super(Peer.group(0), order, null, ""); + } + + + } + + @Override + public void onPause() { + super.onPause(); + hideSearch(); + } + protected abstract void onPeerPicked(Peer peer); } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/search/SearchAdapter.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/search/SearchAdapter.java index fa9e3c049c..749ac79e63 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/search/SearchAdapter.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/search/SearchAdapter.java @@ -1,10 +1,15 @@ package im.actor.sdk.controllers.search; import android.content.Context; +import android.support.v7.widget.RecyclerView; import android.view.ViewGroup; +import android.widget.TextView; import im.actor.core.entity.SearchEntity; import im.actor.runtime.generic.mvvm.BindedDisplayList; +import im.actor.sdk.ActorSDK; +import im.actor.sdk.R; +import im.actor.sdk.util.Screen; import im.actor.sdk.view.adapters.OnItemClickedListener; import im.actor.runtime.android.view.BindedListAdapter; @@ -25,13 +30,53 @@ public void setQuery(String query) { this.query = query; } + @Override + public int getItemViewType(int position) { + SearchEntity e = getItem(position); + if (e instanceof GlobalSearchBaseFragment.SearchEntityHeader) { + return 1; + } else { + return super.getItemViewType(position); + } + } + @Override public SearchHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { - return new SearchHolder(context, onItemClickedListener); + switch (viewType) { + case 1: + return new SearchHolderEx(context, onItemClickedListener); + default: + case 0: + return new SearchHolder(context, onItemClickedListener); + } } @Override public void onBindViewHolder(SearchHolder dialogHolder, int index, SearchEntity item) { dialogHolder.bind(item, query, index == getItemCount() - 1); } + + public class SearchHolderEx extends SearchHolder { + public SearchHolderEx(Context context, OnItemClickedListener clickedListener) { + super(context, clickedListener); + } + + @Override + protected void init(Context context, OnItemClickedListener clickedListener) { + itemView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + itemView.setBackgroundColor(ActorSDK.sharedActor().style.getBackyardBackgroundColor()); + TextView globalSearchTitle = new TextView(context); + globalSearchTitle.setText(R.string.main_search_global_header); + globalSearchTitle.setTextSize(16); + globalSearchTitle.setPadding(Screen.dp(12), Screen.dp(8), 0, Screen.dp(8)); + globalSearchTitle.setBackgroundColor(ActorSDK.sharedActor().style.getBackyardBackgroundColor()); + globalSearchTitle.setTextColor(ActorSDK.sharedActor().style.getTextSecondaryColor()); + ((ViewGroup) itemView).addView(globalSearchTitle); + } + + @Override + public void bind(SearchEntity entity, String query, boolean isLast) { + } + } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/search/SearchHolder.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/search/SearchHolder.java index 89cb375049..abb6297ebc 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/search/SearchHolder.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/search/SearchHolder.java @@ -30,7 +30,10 @@ public class SearchHolder extends BindedViewHolder { public SearchHolder(Context context, final OnItemClickedListener clickedListener) { super(new FrameLayout(context)); + init(context, clickedListener); + } + protected void init(Context context, final OnItemClickedListener clickedListener) { itemView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); @@ -83,9 +86,12 @@ public void onClick(View v) { } public void bind(SearchEntity entity, String query, boolean isLast) { + boolean needRebind = this.entity == null || !entity.getPeer().equals(this.entity.getPeer()); this.entity = entity; - avatar.bind(entity.getAvatar(), entity.getTitle(), entity.getPeer().getPeerId()); + if (needRebind) { + avatar.bind(entity.getAvatar(), entity.getTitle(), entity.getPeer().getPeerId()); + } if (query != null) { title.setText(SearchHighlight.highlightQuery(entity.getTitle(), query, highlightColor)); } else { diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/BaseActorSettingsFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/BaseActorSettingsFragment.java index 9e1f64a049..025270616d 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/BaseActorSettingsFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/BaseActorSettingsFragment.java @@ -1,5 +1,6 @@ package im.actor.sdk.controllers.settings; +import android.app.Activity; import android.app.AlertDialog; import android.content.ClipData; import android.content.ClipboardManager; @@ -66,6 +67,7 @@ public abstract class BaseActorSettingsFragment extends BaseFragment implements IActorSettingsFragment { + private boolean animateToolbar = true; private int baseColor; private AvatarView avatarView; protected SharedPreferences shp; @@ -74,10 +76,24 @@ public abstract class BaseActorSettingsFragment extends BaseFragment implements private boolean noEmails = false; private HeaderViewRecyclerAdapter wallpaperAdapter; + public BaseActorSettingsFragment() { + setHasOptionsMenu(true); + } + + public boolean isAnimateToolbar() { + return animateToolbar; + } + + public void setAnimateToolbar(boolean animateToolbar) { + this.animateToolbar = animateToolbar; + } + @Override public void onCreate(Bundle saveInstance) { super.onCreate(saveInstance); - setHasOptionsMenu(true); + if (saveInstance != null) { + animateToolbar = saveInstance.getBoolean("animateToolbar", true); + } } @Override @@ -121,6 +137,7 @@ public void onChanged(final String val, Value Value) { final View recordView = inflater.inflate(R.layout.contact_record, nickContainer, false); ImageView nickIcon = (ImageView) recordView.findViewById(R.id.recordIcon); Drawable drawable = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_mention_24_dp)); + drawable.mutate(); DrawableCompat.setTint(drawable, style.getSettingsIconColor()); nickIcon.setImageDrawable(drawable); @@ -149,6 +166,7 @@ public void onClick(View v) { final TextView aboutTitle = (TextView) about.findViewById(R.id.value); ImageView nickIcon = (ImageView) about.findViewById(R.id.recordIcon); Drawable drawable = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_info_black_24dp)); + drawable.mutate(); DrawableCompat.setTint(drawable, style.getSettingsIconColor()); nickIcon.setImageDrawable(drawable); aboutTitle.setTextColor(style.getTextPrimaryColor()); @@ -285,6 +303,7 @@ public void onChanged(ArrayListUserEmail val, Value Value) { ImageView tintImageView = (ImageView) recordView.findViewById(R.id.recordIcon); if (i == 0) { Drawable drawable = DrawableCompat.wrap(getResources().getDrawable(R.drawable.ic_email_white_24dp)); + drawable.mutate(); DrawableCompat.setTint(drawable, style.getSettingsIconColor()); tintImageView.setImageDrawable(drawable); } else { @@ -561,6 +580,7 @@ public void onClick(View view) { } view.findViewById(R.id.avatarContainer).setBackgroundColor(style.getToolBarColor()); + avatarView = (AvatarView) view.findViewById(R.id.avatar); avatarView.init(Screen.dp(96), 44); avatarView.bind(users().get(myUid())); @@ -575,8 +595,7 @@ public void onClick(View view) { icon.setImageResource(R.drawable.ic_image_black_24dp); icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); icon.setColorFilter(ActorSDK.sharedActor().style.getSettingsIconColor(), PorterDuff.Mode.SRC_IN); - icon.setPadding(Screen.dp(16), 0, 0, 0); - fl.addView(icon, new FrameLayout.LayoutParams(Screen.dp(40), Screen.dp(85), Gravity.CENTER_VERTICAL | Gravity.LEFT)); + fl.addView(icon, new FrameLayout.LayoutParams(Screen.dp(72), Screen.dp(85), Gravity.CENTER)); fl.setLayoutParams(new ViewGroup.LayoutParams(Screen.dp(72), Screen.dp(85))); wallpaperAdapter.addHeaderView(fl); wallpapers.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false)); @@ -684,8 +703,17 @@ private void addFields(FrameLayout container, ArrayList fiel } private void updateActionBar(int offset) { - + if (!animateToolbar) { + return; + } + Activity activity = getActivity(); + if (!(activity instanceof BaseActivity)) { + return; + } ActionBar bar = ((BaseActivity) getActivity()).getSupportActionBar(); + if (bar == null) { + return; + } int fullColor = baseColor; ActorStyle style = ActorSDK.sharedActor().style; if (style.getToolBarColor() != 0) { @@ -747,6 +775,12 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean("animateToolbar", animateToolbar); + } + @Override public View getBeforeNickSettingsView() { return null; diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/BlockedListActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/BlockedListActivity.java index ad10540d9c..dc745ba363 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/BlockedListActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/BlockedListActivity.java @@ -14,7 +14,7 @@ protected void onCreate(Bundle savedInstanceState) { if (savedInstanceState == null) { - showFragment(new BlockedListFragment(), false, false); + showFragment(new BlockedListFragment(), false); } } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/ChatSettingsActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/ChatSettingsActivity.java index eefc14e4d4..d28b5507a6 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/ChatSettingsActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/ChatSettingsActivity.java @@ -24,7 +24,7 @@ protected void onCreate(Bundle savedInstanceState) { } if (savedInstanceState == null) { - showFragment(fragment, false, false); + showFragment(fragment, false); } } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/ChatSettingsFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/ChatSettingsFragment.java index 554fcd6a5f..6fc0558496 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/ChatSettingsFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/ChatSettingsFragment.java @@ -36,20 +36,35 @@ public void onClick(View v) { ((TextView) res.findViewById(R.id.settings_send_by_enter_title)).setTextColor(ActorSDK.sharedActor().style.getTextPrimaryColor()); ((TextView) res.findViewById(R.id.settings_set_by_enter_hint)).setTextColor(ActorSDK.sharedActor().style.getTextSecondaryColor()); - final CheckBox animationsAtoPlay = (CheckBox) res.findViewById(R.id.animationAutoPlay); - animationsAtoPlay.setChecked(messenger().isAnimationAutoPlayEnabled()); - View.OnClickListener animListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - messenger().changeAnimationAutoPlayEnabled(!messenger().isAnimationAutoPlayEnabled()); - animationsAtoPlay.setChecked(messenger().isAnimationAutoPlayEnabled()); - } - }; - animationsAtoPlay.setOnClickListener(animListener); - res.findViewById(R.id.animationAutoPlayCont).setOnClickListener(animListener); - ((TextView) res.findViewById(R.id.settings_animation_auto_play_title)).setTextColor(ActorSDK.sharedActor().style.getTextPrimaryColor()); + setupCheckbox(res, R.id.animationAutoPlay, R.id.animationAutoPlayCont, R.id.settings_animation_auto_play_title, () -> messenger().changeAnimationAutoPlayEnabled(!messenger().isAnimationAutoPlayEnabled()), () -> messenger().isAnimationAutoPlayEnabled()); ((TextView) res.findViewById(R.id.settings_animation_auto_play_hint)).setTextColor(ActorSDK.sharedActor().style.getTextSecondaryColor()); + setupCheckbox(res, R.id.animationAutoDownload, R.id.animationAutoDownloadCont, R.id.settings_animation_download_title, () -> messenger().changeAnimationAutoDownloadEnabled(!messenger().isAnimationAutoDownloadEnabled()), () -> messenger().isAnimationAutoDownloadEnabled()); + setupCheckbox(res, R.id.imageAutoDownload, R.id.imageAutoDownloadCont, R.id.settings_image_download_title, () -> messenger().changeImageAutoDownloadEnabled(!messenger().isImageAutoDownloadEnabled()), () -> messenger().isImageAutoDownloadEnabled()); + setupCheckbox(res, R.id.videoAutoDownload, R.id.videoAutoDownloadCont, R.id.settings_video_download_title, () -> messenger().changeVideoAutoDownloadEnabled(!messenger().isVideoAutoDownloadEnabled()), () -> messenger().isVideoAutoDownloadEnabled()); + setupCheckbox(res, R.id.audioAutoDownload, R.id.audioAutoDownloadCont, R.id.settings_audio_download_title, () -> messenger().changeAudioAutoDownloadEnabled(!messenger().isAudioAutoDownloadEnabled()), () -> messenger().isAudioAutoDownloadEnabled()); + setupCheckbox(res, R.id.docAutoDownload, R.id.docAutoDownloadCont, R.id.settings_doc_download_title, () -> messenger().changeDocAutoDownloadEnabled(!messenger().isDocAutoDownloadEnabled()), () -> messenger().isDocAutoDownloadEnabled()); + return res; } + + protected void setupCheckbox(View root, int chbId, int contId, int titleId, OnClLstnr lstnr, Checker checker) { + final CheckBox animationsAtoPlay = (CheckBox) root.findViewById(chbId); + animationsAtoPlay.setChecked(checker.check()); + View.OnClickListener animListener = v -> { + lstnr.onClick(); + animationsAtoPlay.setChecked(checker.check()); + }; + animationsAtoPlay.setOnClickListener(animListener); + root.findViewById(contId).setOnClickListener(animListener); + ((TextView) root.findViewById(titleId)).setTextColor(ActorSDK.sharedActor().style.getTextPrimaryColor()); + } + + private interface OnClLstnr { + void onClick(); + } + + private interface Checker { + boolean check(); + } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/EditAboutActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/EditAboutActivity.java index 66a3644091..d239b649af 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/EditAboutActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/EditAboutActivity.java @@ -25,7 +25,7 @@ protected void onCreate(Bundle savedInstanceState) { } if (savedInstanceState == null) { - showFragment(EditAboutFragment.editAbout(type, id), false, false); + showFragment(EditAboutFragment.editAbout(type, id), false); } } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/EditAboutFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/EditAboutFragment.java index de509f600f..414ddadba2 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/EditAboutFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/EditAboutFragment.java @@ -76,8 +76,7 @@ public void onClick(View v) { public void onClick(View v) { String about = aboutEdit.getText().toString().trim(); if (about.length() == 0) { - Toast.makeText(getActivity(), R.string.toast_empty_about, Toast.LENGTH_SHORT).show(); - return; + about = null; } if (type == EditAboutActivity.TYPE_ME) { @@ -93,18 +92,6 @@ public void onError(Exception e) { } }); //TODO: set group about - } else if (type == EditAboutActivity.TYPE_GROUP) { - execute(messenger().editGroupAbout(id, about), R.string.edit_about_process, new CommandCallback() { - @Override - public void onResult(Void res) { - getActivity().finish(); - } - - @Override - public void onError(Exception e) { - Toast.makeText(getActivity(), R.string.toast_unable_change, Toast.LENGTH_SHORT).show(); - } - }); } } }); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/EditNameActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/EditNameActivity.java index 790715da2d..b6c5b46fe9 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/EditNameActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/EditNameActivity.java @@ -34,7 +34,7 @@ protected void onCreate(Bundle savedInstanceState) { } if (savedInstanceState == null) { - showFragment(EditNameFragment.editName(type, id), false, false); + showFragment(EditNameFragment.editName(type, id), false); } } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/EditNameFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/EditNameFragment.java index be9caad67a..6b31e34bbe 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/EditNameFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/EditNameFragment.java @@ -120,18 +120,6 @@ public void onResult(Boolean res) { getActivity().finish(); } - @Override - public void onError(Exception e) { - Toast.makeText(getActivity(), R.string.toast_unable_change, Toast.LENGTH_SHORT).show(); - } - }); - } else if (type == EditNameActivity.TYPE_GROUP) { - execute(messenger().editGroupTitle(id, name), R.string.edit_name_process, new CommandCallback() { - @Override - public void onResult(Void res) { - getActivity().finish(); - } - @Override public void onError(Exception e) { Toast.makeText(getActivity(), R.string.toast_unable_change, Toast.LENGTH_SHORT).show(); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/MyProfileActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/MyProfileActivity.java index ab18972c6e..a135b99c26 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/MyProfileActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/MyProfileActivity.java @@ -30,7 +30,7 @@ protected void onCreate(Bundle savedInstanceState) { fragment = new ActorSettingsFragment(); } - showFragment(fragment, false, false); + showFragment(fragment, false); } } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/NotificationsActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/NotificationsActivity.java index dbcf1355ce..ca1354e1b1 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/NotificationsActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/NotificationsActivity.java @@ -14,7 +14,7 @@ protected void onCreate(Bundle savedInstanceState) { getSupportActionBar().setTitle(R.string.not_title); if (savedInstanceState == null) { - showFragment(new NotificationsFragment(), false, false); + showFragment(new NotificationsFragment(), false); } } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/NotificationsFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/NotificationsFragment.java index df28494ca0..2e5dca1ff7 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/NotificationsFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/NotificationsFragment.java @@ -1,6 +1,11 @@ package im.actor.sdk.controllers.settings; +import android.app.Activity; +import android.content.Intent; +import android.media.RingtoneManager; +import android.net.Uri; import android.os.Bundle; +import android.provider.Settings; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -11,30 +16,25 @@ import im.actor.sdk.ActorStyle; import im.actor.sdk.R; import im.actor.sdk.controllers.BaseFragment; +import im.actor.sdk.util.ViewUtils; import static im.actor.sdk.util.ActorSDKMessenger.messenger; public class NotificationsFragment extends BaseFragment { + public static int SOUND_PICKER_REQUEST_CODE = 122; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View res = inflater.inflate(R.layout.fr_settings_notifications, container, false); res.setBackgroundColor(ActorSDK.sharedActor().style.getMainBackgroundColor()); - res.findViewById(R.id.divider).setBackgroundColor(ActorSDK.sharedActor().style.getDividerColor()); - res.findViewById(R.id.divider1).setBackgroundColor(ActorSDK.sharedActor().style.getDividerColor()); - res.findViewById(R.id.divider2).setBackgroundColor(ActorSDK.sharedActor().style.getDividerColor()); - res.findViewById(R.id.divider3).setBackgroundColor(ActorSDK.sharedActor().style.getDividerColor()); - res.findViewById(R.id.divider4).setBackgroundColor(ActorSDK.sharedActor().style.getDividerColor()); // Conversation tone final CheckBox enableTones = (CheckBox) res.findViewById(R.id.enableConversationTones); enableTones.setChecked(messenger().isConversationTonesEnabled()); - View.OnClickListener enableTonesListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - messenger().changeConversationTonesEnabled(!messenger().isConversationTonesEnabled()); - enableTones.setChecked(messenger().isConversationTonesEnabled()); - } + View.OnClickListener enableTonesListener = v -> { + messenger().changeConversationTonesEnabled(!messenger().isConversationTonesEnabled()); + enableTones.setChecked(messenger().isConversationTonesEnabled()); }; ActorStyle style = ActorSDK.sharedActor().style; ((TextView) res.findViewById(R.id.settings_conversation_tones_title)).setTextColor(style.getTextPrimaryColor()); @@ -42,77 +42,126 @@ public void onClick(View v) { enableTones.setOnClickListener(enableTonesListener); res.findViewById(R.id.conversationTonesCont).setOnClickListener(enableTonesListener); - // Sound - final CheckBox enableSound = (CheckBox) res.findViewById(R.id.enableSound); - enableSound.setChecked(messenger().isNotificationSoundEnabled()); - View.OnClickListener enableSoundListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - messenger().changeNotificationSoundEnabled(!messenger().isNotificationSoundEnabled()); - enableSound.setChecked(messenger().isNotificationSoundEnabled()); - } - }; - enableSound.setOnClickListener(enableSoundListener); - res.findViewById(R.id.soundCont).setOnClickListener(enableSoundListener); - ((TextView) res.findViewById(R.id.settings_sound_title)).setTextColor(style.getTextPrimaryColor()); - ((TextView) res.findViewById(R.id.settings_sound_hint)).setTextColor(style.getTextSecondaryColor()); // Vibration final CheckBox enableVibration = (CheckBox) res.findViewById(R.id.enableVibration); enableVibration.setChecked(messenger().isNotificationVibrationEnabled()); - View.OnClickListener enableVibrationListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - messenger().changeNotificationVibrationEnabled(!messenger().isNotificationVibrationEnabled()); - enableVibration.setChecked(messenger().isNotificationVibrationEnabled()); - } + View.OnClickListener enableVibrationListener = v -> { + messenger().changeNotificationVibrationEnabled(!messenger().isNotificationVibrationEnabled()); + enableVibration.setChecked(messenger().isNotificationVibrationEnabled()); }; enableVibration.setOnClickListener(enableVibrationListener); res.findViewById(R.id.vibrationCont).setOnClickListener(enableVibrationListener); ((TextView) res.findViewById(R.id.settings_vibration_title)).setTextColor(style.getTextPrimaryColor()); ((TextView) res.findViewById(R.id.settings_vibration_hint)).setTextColor(style.getTextSecondaryColor()); + // Group final CheckBox enableGroup = (CheckBox) res.findViewById(R.id.enableGroup); enableGroup.setChecked(messenger().isGroupNotificationsEnabled()); - View.OnClickListener enableGroupListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - messenger().changeGroupNotificationsEnabled(!messenger().isGroupNotificationsEnabled()); - enableGroup.setChecked(messenger().isGroupNotificationsEnabled()); - } + View.OnClickListener enableGroupListener = v -> { + messenger().changeGroupNotificationsEnabled(!messenger().isGroupNotificationsEnabled()); + enableGroup.setChecked(messenger().isGroupNotificationsEnabled()); }; enableGroup.setOnClickListener(enableGroupListener); res.findViewById(R.id.groupCont).setOnClickListener(enableGroupListener); ((TextView) res.findViewById(R.id.settings_group_title)).setTextColor(style.getTextPrimaryColor()); ((TextView) res.findViewById(R.id.settings_group_hint)).setTextColor(style.getTextSecondaryColor()); + // Mentions final CheckBox enableGroupMentions = (CheckBox) res.findViewById(R.id.enableGroupMentions); enableGroupMentions.setChecked(messenger().isGroupNotificationsOnlyMentionsEnabled()); - View.OnClickListener enableGroupMentionsListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - messenger().changeGroupNotificationsOnlyMentionsEnabled(!messenger().isGroupNotificationsOnlyMentionsEnabled()); - enableGroupMentions.setChecked(messenger().isGroupNotificationsOnlyMentionsEnabled()); - } + View.OnClickListener enableGroupMentionsListener = v -> { + messenger().changeGroupNotificationsOnlyMentionsEnabled(!messenger().isGroupNotificationsOnlyMentionsEnabled()); + enableGroupMentions.setChecked(messenger().isGroupNotificationsOnlyMentionsEnabled()); }; enableGroupMentions.setOnClickListener(enableGroupMentionsListener); res.findViewById(R.id.groupMentionsCont).setOnClickListener(enableGroupMentionsListener); ((TextView) res.findViewById(R.id.settings_group_mentions_title)).setTextColor(style.getTextPrimaryColor()); ((TextView) res.findViewById(R.id.settings_group_mentions_hint)).setTextColor(style.getTextSecondaryColor()); - // Names and messages + // Names and messages final CheckBox enableText = (CheckBox) res.findViewById(R.id.enableTitles); enableText.setChecked(messenger().isShowNotificationsText()); - View.OnClickListener enableTextListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - messenger().changeShowNotificationTextEnabled(!messenger().isShowNotificationsText()); - enableText.setChecked(messenger().isShowNotificationsText()); - } + View.OnClickListener enableTextListener = v -> { + messenger().changeShowNotificationTextEnabled(!messenger().isShowNotificationsText()); + enableText.setChecked(messenger().isShowNotificationsText()); }; enableText.setOnClickListener(enableTextListener); res.findViewById(R.id.titlesCont).setOnClickListener(enableTextListener); ((TextView) res.findViewById(R.id.settings_titles_title)).setTextColor(style.getTextPrimaryColor()); ((TextView) res.findViewById(R.id.settings_titles_hint)).setTextColor(style.getTextSecondaryColor()); + + // Sound + View soundPickerCont = res.findViewById(R.id.soundPickerCont); + View soundPickerDivider = res.findViewById(R.id.divider); + + if (messenger().isNotificationSoundEnabled()) { + ViewUtils.showViews(false, soundPickerCont, soundPickerDivider); + } else { + ViewUtils.goneViews(false, soundPickerCont, soundPickerDivider); + } + + final CheckBox enableSound = (CheckBox) res.findViewById(R.id.enableSound); + enableSound.setChecked(messenger().isNotificationSoundEnabled()); + View.OnClickListener enableSoundListener = v -> { + messenger().changeNotificationSoundEnabled(!messenger().isNotificationSoundEnabled()); + enableSound.setChecked(messenger().isNotificationSoundEnabled()); + + //show/hide sound picker + if (messenger().isNotificationSoundEnabled()) { + ViewUtils.showViews(soundPickerCont, soundPickerDivider); + } else { + ViewUtils.goneViews(soundPickerCont, soundPickerDivider); + } + }; + enableSound.setOnClickListener(enableSoundListener); + res.findViewById(R.id.soundCont).setOnClickListener(enableSoundListener); + ((TextView) res.findViewById(R.id.settings_sound_title)).setTextColor(style.getTextPrimaryColor()); + ((TextView) res.findViewById(R.id.settings_sound_hint)).setTextColor(style.getTextSecondaryColor()); + + // Sound picker + View.OnClickListener soundPickerListener = v -> { + Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)); + + Uri currentSound = null; + String defaultPath = null; + Uri defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI; + if (defaultUri != null) { + defaultPath = defaultUri.getPath(); + } + + String path = messenger().getPreferences().getString("globalNotificationSound"); + if (path == null) { + path = defaultPath; + } + if (path != null && !path.equals("none")) { + if (path.equals(defaultPath)) { + currentSound = defaultUri; + } else { + currentSound = Uri.parse(path); + } + } + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, currentSound); + startActivityForResult(intent, SOUND_PICKER_REQUEST_CODE); + }; + res.findViewById(R.id.soundPickerCont).setOnClickListener(soundPickerListener); + ((TextView) res.findViewById(R.id.settings_sound_picker_title)).setTextColor(style.getTextPrimaryColor()); + ((TextView) res.findViewById(R.id.settings_sound_picker_hint)).setTextColor(style.getTextSecondaryColor()); return res; } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK && requestCode == SOUND_PICKER_REQUEST_CODE) { + Uri ringtone = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); + + if (ringtone != null) { + messenger().getPreferences().putString("globalNotificationSound", ringtone.toString()); + } else { + messenger().getPreferences().putString("globalNotificationSound", "none"); + } + } + } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/PickWallpaperActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/PickWallpaperActivity.java index c6f02a071d..d3e3008348 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/PickWallpaperActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/PickWallpaperActivity.java @@ -16,7 +16,7 @@ protected void onCreate(Bundle savedInstanceState) { getSupportActionBar().setTitle(R.string.wallpaper); if (savedInstanceState == null) { - showFragment(PickWallpaperFragment.chooseWallpaper(id), false, false); + showFragment(PickWallpaperFragment.chooseWallpaper(id), false); } } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/SecuritySettingsActivity.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/SecuritySettingsActivity.java index 8222253a06..5984eae488 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/SecuritySettingsActivity.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/SecuritySettingsActivity.java @@ -23,7 +23,7 @@ protected void onCreate(Bundle savedInstanceState) { } if (savedInstanceState == null) { - showFragment(fragment, false, false); + showFragment(fragment, false); } } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/SecuritySettingsFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/SecuritySettingsFragment.java index 026209e8d5..e7be2501c0 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/SecuritySettingsFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/settings/SecuritySettingsFragment.java @@ -1,7 +1,6 @@ package im.actor.sdk.controllers.settings; import android.app.AlertDialog; -import android.content.DialogInterface; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -11,8 +10,8 @@ import android.widget.Toast; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.List; import im.actor.core.api.ApiAuthHolder; @@ -39,45 +38,48 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa loading = (TextView) res.findViewById(R.id.loading); loading.setTextColor(ActorSDK.sharedActor().style.getTextPrimaryColor()); loading.setVisibility(View.GONE); - loading.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - performLoad(); - } - }); + loading.setOnClickListener(v -> performLoad()); authItems = (LinearLayout) res.findViewById(R.id.authItems); - res.findViewById(R.id.divider).setBackgroundColor(ActorSDK.sharedActor().style.getDividerColor()); - res.findViewById(R.id.terminateSessions).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - new AlertDialog.Builder(getActivity()) - .setMessage(R.string.security_terminate_message) - .setPositiveButton(R.string.dialog_yes, new DialogInterface.OnClickListener() { + ((TextView) res.findViewById(R.id.settings_last_seen_title)).setTextColor(ActorSDK.sharedActor().style.getTextPrimaryColor()); + ((TextView) res.findViewById(R.id.settings_last_seen_hint)).setTextColor(ActorSDK.sharedActor().style.getTextSecondaryColor()); + + res.findViewById(R.id.lastSeen).setOnClickListener(v -> { + String[] itemsValues = new String[]{"always", "contacts", "none"}; + String[] items = new String[]{getString(R.string.security_last_seen_everybody), getString(R.string.security_last_seen_contacts), getString(R.string.security_last_seen_nobody)}; + int currentLastSeen = Arrays.asList(itemsValues).indexOf(messenger().getPrivacy()); + + new AlertDialog.Builder(getActivity()) + .setTitle(R.string.security_last_seen_title) + .setSingleChoiceItems(items, currentLastSeen, (dialog, which) -> { + messenger().setPrivacy(itemsValues[which]); + dialog.dismiss(); + }) + .setNegativeButton(R.string.dialog_cancel, null) + .setPositiveButton(R.string.dialog_ok, null) + .show(); + }); + + res.findViewById(R.id.terminateSessions).setOnClickListener(v -> new AlertDialog.Builder(getActivity()) + .setMessage(R.string.security_terminate_message) + .setPositiveButton(R.string.dialog_yes, (dialog, which) -> execute(messenger().terminateAllSessions(), R.string.progress_common, + new CommandCallback() { @Override - public void onClick(DialogInterface dialog, int which) { - execute(messenger().terminateAllSessions(), R.string.progress_common, - new CommandCallback() { - @Override - public void onResult(Void res) { - performLoad(); - } + public void onResult(Void res1) { + performLoad(); + } - @Override - public void onError(Exception e) { - performLoad(); - Toast.makeText(getActivity(), - R.string.security_toast_unable_remove_auth, Toast.LENGTH_SHORT) - .show(); - } - }); + @Override + public void onError(Exception e) { + performLoad(); + Toast.makeText(getActivity(), + R.string.security_toast_unable_remove_auth, Toast.LENGTH_SHORT) + .show(); } - }) - .setNegativeButton(R.string.dialog_no, null) - .show() - .setCanceledOnTouchOutside(true); - } - }); + })) + .setNegativeButton(R.string.dialog_no, null) + .show() + .setCanceledOnTouchOutside(true)); ((TextView) res.findViewById(R.id.settings_terminate_sessions_title)).setTextColor(ActorSDK.sharedActor().style.getTextPrimaryColor()); ((TextView) res.findViewById(R.id.settings_terminate_sessions_hint)).setTextColor(ActorSDK.sharedActor().style.getTextSecondaryColor()); performLoad(); @@ -96,12 +98,7 @@ public void onResult(List res) { goneView(loading, false); authItems.removeAllViews(); ArrayList items = new ArrayList(res); - Collections.sort(items, new Comparator() { - @Override - public int compare(ApiAuthSession lhs, ApiAuthSession rhs) { - return rhs.getAuthTime() - lhs.getAuthTime(); - } - }); + Collections.sort(items, (lhs, rhs) -> rhs.getAuthTime() - lhs.getAuthTime()); for (final ApiAuthSession item : items) { if (getActivity() == null) return; View view = getActivity().getLayoutInflater().inflate(R.layout.adapter_auth, authItems, false); @@ -115,34 +112,24 @@ public int compare(ApiAuthSession lhs, ApiAuthSession rhs) { ((TextView) view.findViewById(R.id.deviceTitle)).setText(deviceTitle); ((TextView) view.findViewById(R.id.deviceTitle)).setTextColor(ActorSDK.sharedActor().style.getTextPrimaryColor()); if (!isThisDevice) { - view.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - new AlertDialog.Builder(getActivity()) - .setMessage(getString(R.string.security_terminate_this_message).replace("{device}", item.getDeviceTitle() )) - .setPositiveButton(R.string.dialog_yes, new DialogInterface.OnClickListener() { + view.setOnClickListener(v -> new AlertDialog.Builder(getActivity()) + .setMessage(getString(R.string.security_terminate_this_message).replace("{device}", item.getDeviceTitle())) + .setPositiveButton(R.string.dialog_yes, (dialog, which) -> execute(messenger().terminateSession(item.getId()), R.string.progress_common, + new CommandCallback() { @Override - public void onClick(DialogInterface dialog, int which) { - execute(messenger().terminateSession(item.getId()), R.string.progress_common, - new CommandCallback() { - @Override - public void onResult(Void res) { - performLoad(); - } - - @Override - public void onError(Exception e) { - Toast.makeText(getActivity(), R.string.security_toast_unable_remove_auth , Toast.LENGTH_SHORT).show(); - performLoad(); - } - }); + public void onResult(Void res1) { + performLoad(); } - }) - .setNegativeButton(R.string.dialog_no, null) - .show() - .setCanceledOnTouchOutside(true); - } - }); + + @Override + public void onError(Exception e) { + Toast.makeText(getActivity(), R.string.security_toast_unable_remove_auth, Toast.LENGTH_SHORT).show(); + performLoad(); + } + })) + .setNegativeButton(R.string.dialog_no, null) + .show() + .setCanceledOnTouchOutside(true)); } authItems.addView(view); } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/share/ShareFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/share/ShareFragment.java index 7769168e00..1d7bf0087d 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/share/ShareFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/share/ShareFragment.java @@ -16,6 +16,7 @@ import im.actor.core.entity.Peer; import im.actor.core.entity.PeerType; import im.actor.core.entity.content.AbsContent; +import im.actor.sdk.ActorSDK; import im.actor.sdk.R; import im.actor.sdk.controllers.Intents; import im.actor.sdk.controllers.dialogs.DialogsFragment; @@ -139,7 +140,7 @@ public void onPeerClicked(Peer peer) { messenger().sendMessage(peer, shareAction.getText()); } else if (shareAction.getUris().size() > 0) { for (String sendUri : shareAction.getUris()) { - executeSilent(messenger().sendUri(peer, Uri.parse(sendUri))); + executeSilent(messenger().sendUri(peer, Uri.parse(sendUri), ActorSDK.sharedActor().getAppName())); } } else if (shareAction.getUserId() != null) { String userName = users().get(shareAction.getUserId()).getName().get(); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/tools/InviteHandler.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/tools/InviteHandler.java new file mode 100644 index 0000000000..7d0434e29c --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/tools/InviteHandler.java @@ -0,0 +1,111 @@ +package im.actor.sdk.controllers.tools; + +import android.content.DialogInterface; +import android.content.Intent; +import android.support.v7.app.AlertDialog; +import android.widget.Toast; + +import java.io.UnsupportedEncodingException; + +import im.actor.core.entity.Peer; +import im.actor.core.viewmodel.CommandCallback; +import im.actor.core.viewmodel.GroupVM; +import im.actor.runtime.HTTP; +import im.actor.runtime.function.Consumer; +import im.actor.runtime.http.HTTPResponse; +import im.actor.runtime.json.JSONException; +import im.actor.runtime.json.JSONObject; +import im.actor.sdk.ActorSDK; +import im.actor.sdk.R; +import im.actor.sdk.controllers.Intents; +import im.actor.sdk.controllers.activity.BaseActivity; + +import static im.actor.sdk.util.ActorSDKMessenger.groups; +import static im.actor.sdk.util.ActorSDKMessenger.messenger; + +public class InviteHandler { + + public static void handleIntent(BaseActivity activity, Intent intent) { + if (intent.getAction() != null && intent.getAction().equals(Intent.ACTION_VIEW) && intent.getData() != null) { + String joinGroupUrl = intent.getData().toString(); + if (joinGroupUrl != null && (joinGroupUrl.contains("join") || joinGroupUrl.contains("token"))) { + String[] urlSplit = null; + if (joinGroupUrl.contains("join")) { + urlSplit = joinGroupUrl.split("/join/"); + } else if (joinGroupUrl.contains("token")) { + urlSplit = joinGroupUrl.split("token="); + } + if (urlSplit != null) { + joinGroupUrl = urlSplit[urlSplit.length - 1]; + + final String token = joinGroupUrl; + HTTP.getMethod(ActorSDK.sharedActor().getInviteDataUrl() + joinGroupUrl, 0, 0, 0).then(new Consumer() { + @Override + public void apply(HTTPResponse httpResponse) { + try { + JSONObject data = new JSONObject(new String(httpResponse.getContent(), "UTF-8")); + JSONObject group = data.getJSONObject("group"); + String title = group.getString("title"); + if (group.has("id") && group.has("isPublic")) { + int gid = group.getInt("id"); + boolean isPublic = group.getBoolean("isPublic"); + //Check if we have this group + try { + GroupVM groupVM = groups().get(gid); + if (groupVM.isMember().get() || isPublic) { + //Have this group, is member or group is public, just open it + activity.startActivity(Intents.openDialog(Peer.group(gid), false, activity)); + } else { + //Have this group, but not member, join it + joinViaToken(token, title, activity); + } + } catch (Exception e) { + //Do not have this group, join it + if (isPublic) { + messenger().findPublicGroupById(gid).then(peer -> activity.startActivity(Intents.openDialog(peer, false, activity))); + } else { + joinViaToken(token, title, activity); + } + } + } else { + joinViaToken(token, title, activity); + } + } catch (JSONException | UnsupportedEncodingException e) { + e.printStackTrace(); + } + } + }); + } + } + } + } + + private static void joinViaToken(String joinGroupUrl, String title, BaseActivity activity) { + AlertDialog.Builder b = new AlertDialog.Builder(activity); + b.setTitle(activity.getString(R.string.invite_link_join_confirm, title)) + .setPositiveButton(R.string.dialog_yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + dialogInterface.dismiss(); + activity.execute(messenger().joinGroupViaToken(joinGroupUrl), R.string.invite_link_title, new CommandCallback() { + @Override + public void onResult(Integer res) { + activity.startActivity(Intents.openGroupDialog(res, true, activity)); + } + + @Override + public void onError(Exception e) { + Toast.makeText(activity, e.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } + + }) + .setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + dialogInterface.dismiss(); + } + }).show(); + } +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/tools/MediaPickerCallback.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/tools/MediaPickerCallback.java index d656357bb2..10885ebe83 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/tools/MediaPickerCallback.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/tools/MediaPickerCallback.java @@ -5,6 +5,7 @@ import java.util.List; public interface MediaPickerCallback { + void onUriPicked(Uri uri); void onFilesPicked(List paths); @@ -13,6 +14,8 @@ public interface MediaPickerCallback { void onVideoPicked(String path); + void onPhotoCropped(String path); + void onContactPicked(String name, List phones, List emails, byte[] avatar); void onLocationPicked(double latitude, double longitude, String street, String place); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/tools/MediaPickerFragment.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/tools/MediaPickerFragment.java index 9762ae9d86..b37df569e0 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/tools/MediaPickerFragment.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/controllers/tools/MediaPickerFragment.java @@ -18,6 +18,8 @@ import android.support.v4.content.ContextCompat; import android.widget.Toast; +import com.soundcloud.android.crop.Crop; + import java.io.File; import java.util.ArrayList; import java.util.List; @@ -38,11 +40,17 @@ public class MediaPickerFragment extends BaseFragment { private static final int REQUEST_LOCATION = 4; private static final int REQUEST_CONTACT = 5; private static final int PERMISSIONS_REQUEST_CAMERA = 6; + private static final int PERMISSIONS_REQUEST_CONTACTS = 7; private String pendingFile; - + private boolean pickCropped; public void requestPhoto() { + requestPhoto(false); + } + + public void requestPhoto(boolean pickCropped) { + this.pickCropped = pickCropped; // // Checking permissions @@ -80,6 +88,7 @@ public void requestPhoto() { } public void requestVideo() { + this.pickCropped = false; // // Generating Temporary File Name @@ -99,22 +108,50 @@ public void requestVideo() { } public void requestGallery() { + requestGallery(false); + } + + public void requestGallery(boolean pickCropped) { + this.pickCropped = pickCropped; + Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); intent.setType("image/* video/*"); startActivityForResult(intent, REQUEST_GALLERY); } public void requestFile() { + this.pickCropped = false; + Activity activity = getActivity(); startActivityForResult(new Intent(activity, FilePickerActivity.class), REQUEST_DOC); } public void requestLocation() { + this.pickCropped = false; + Intent intent = new Intent("im.actor.pickLocation_" + AndroidContext.getContext().getPackageName()); startActivityForResult(intent, REQUEST_LOCATION); } public void requestContact() { + this.pickCropped = false; + + // + // Checking permissions + // + Activity activity = getActivity(); + if (activity != null) { + if (Build.VERSION.SDK_INT >= 23) { + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, + PERMISSIONS_REQUEST_CONTACTS); + return; + } + } + } else { + return; + } + Intent intent = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI); startActivityForResult(intent, REQUEST_CONTACT); } @@ -125,17 +162,37 @@ public void onActivityResult(int requestCode, int resultCode, final Intent data) if (resultCode == Activity.RESULT_OK) { if (requestCode == REQUEST_GALLERY) { if (data.getData() != null) { - getCallback().onUriPicked(data.getData()); + if (pickCropped) { + pendingFile = generateRandomFile(".jpg"); + Crop.of(data.getData(), Uri.fromFile(new File(pendingFile))) + .asSquare() + .start(getContext(), this); + } else { + getCallback().onUriPicked(data.getData()); + } } } else if (requestCode == REQUEST_PHOTO) { if (pendingFile != null) { - getCallback().onPhotoPicked(pendingFile); + + String sourceFileName = pendingFile; Context context = getContext(); if (context != null) { MediaScannerConnection.scanFile(context, new String[]{pendingFile}, new String[]{"image/jpeg"}, null); } - pendingFile = null; + + if (pickCropped) { + pendingFile = generateRandomFile(".jpg"); + Crop.of(Uri.fromFile(new File(sourceFileName)), Uri.fromFile(new File(pendingFile))) + .asSquare() + .start(getContext(), this); + } else { + getCallback().onPhotoPicked(sourceFileName); + } + } + } else if (requestCode == Crop.REQUEST_CROP) { + if (pendingFile != null) { + getCallback().onPhotoCropped(pendingFile); } } else if (requestCode == REQUEST_VIDEO) { if (pendingFile != null) { @@ -227,8 +284,6 @@ public void onActivityResult(int requestCode, int resultCode, final Intent data) getCallback().onLocationPicked(data.getDoubleExtra("longitude", 0), data.getDoubleExtra("latitude", 0), data.getStringExtra("street"), data.getStringExtra("place")); } } - - } @Override @@ -239,6 +294,12 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis requestPhoto(); } } + + if (requestCode == PERMISSIONS_REQUEST_CONTACTS) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + requestContact(); + } + } } @Override @@ -246,6 +307,7 @@ public void onCreate(Bundle saveInstance) { super.onCreate(saveInstance); if (saveInstance != null) { pendingFile = saveInstance.getString("pendingFile", null); + pickCropped = saveInstance.getBoolean("pickCropped"); } } @@ -255,6 +317,7 @@ public void onSaveInstanceState(Bundle outState) { if (pendingFile != null) { outState.putString("pendingFile", pendingFile); } + outState.putBoolean("pickCropped", pickCropped); } private String generateRandomFile(String ext) { @@ -314,6 +377,13 @@ public void onVideoPicked(String path) { } } + @Override + public void onPhotoCropped(String path) { + if (callback != null) { + callback.onPhotoCropped(path); + } + } + @Override public void onContactPicked(String name, List phones, List emails, byte[] avatar) { if (callback != null) { diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/core/AndroidNotifications.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/core/AndroidNotifications.java index 2c3200fb64..0a925c48cc 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/core/AndroidNotifications.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/core/AndroidNotifications.java @@ -91,7 +91,7 @@ public void onNotification(Messenger messenger, List topNotificati // .setBackground(((BitmapDrawable) AppContext.getContext().getResources().getDrawable(R.drawable.wear_bg)).getBitmap()) // .setHintHideIcon(true)); - final Notification topNotification = topNotifications.get(0); + final Notification topNotification = topNotifications.get(topNotifications.size() - 1); // if (!silentUpdate) { // builder.setTicker(getNotificationTextFull(topNotification, messenger)); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/push/ActorPushService.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/push/ActorPushService.java index cc9d9d9a54..9422256a57 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/push/ActorPushService.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/push/ActorPushService.java @@ -22,7 +22,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.Executors; -import im.actor.core.util.ExponentialBackoff; +import im.actor.runtime.util.ExponentialBackoff; /** * Actor Push service based on MQTT diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/receivers/SDKPushReceiver.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/receivers/SDKPushReceiver.java index 2f7462ce60..9598205974 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/receivers/SDKPushReceiver.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/receivers/SDKPushReceiver.java @@ -16,8 +16,9 @@ public void onPushReceived(String payload) { ActorSDK.sharedActor().waitForReady(); if (data.has("seq")) { int seq = data.getInt("seq"); + int authId = data.optInt("authId"); Log.d("SDKPushReceiver", "Seq Received: " + seq); - ActorSDK.sharedActor().getMessenger().onPushReceived(seq); + ActorSDK.sharedActor().getMessenger().onPushReceived(seq, authId); } else if (data.has("callId")) { Long callId = Long.parseLong(data.getString("callId")); int attempt = 0; diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/util/AlertListBuilder.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/util/AlertListBuilder.java new file mode 100644 index 0000000000..c90e7f6bc2 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/util/AlertListBuilder.java @@ -0,0 +1,65 @@ +package im.actor.sdk.util; + +import android.content.Context; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; + +public class AlertListBuilder { + + private ArrayList items = new ArrayList<>(); + + public AlertListBuilder addItem(String name, @NotNull SelectListener selectListener) { + items.add(new Item(name, selectListener)); + return this; + } + + private void select(int i) { + items.get(i).getOnClickListener().onSelected(); + } + + public CharSequence[] getItems() { + CharSequence[] res = new CharSequence[items.size()]; + for (int i = 0; i < items.size(); i++) { + res[i] = items.get(i).getName(); + } + return res; + } + + private class Item { + int id; + String name; + SelectListener selectListener; + + public Item(String name, @NotNull SelectListener selectListener) { + this.name = name; + this.selectListener = selectListener; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + @NotNull + public SelectListener getOnClickListener() { + return selectListener; + } + } + + public interface SelectListener { + void onSelected(); + } + + public android.app.AlertDialog.Builder build(Context context) { + android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(context); + builder.setItems(getItems(), (dialog, which) -> { + select(which); + }); + return builder; + } +} \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/util/ViewUtils.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/util/ViewUtils.java index 575dd30070..e556112a86 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/util/ViewUtils.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/util/ViewUtils.java @@ -1,14 +1,13 @@ package im.actor.sdk.util; -import android.graphics.Color; -import android.os.Handler; +import android.support.annotation.LayoutRes; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; -import android.view.animation.CycleInterpolator; import android.view.animation.ScaleAnimation; import android.view.animation.Transformation; -import android.widget.AbsListView; import im.actor.sdk.view.MaterialInterpolator; @@ -22,6 +21,16 @@ public static void goneView(final View view, boolean isAnimated) { goneView(view, isAnimated, true); } + public static View inflate(@LayoutRes int id, ViewGroup viewGroup) { + return inflate(id, viewGroup, false); + } + + public static View inflate(@LayoutRes int id, ViewGroup viewGroup, boolean attach) { + return LayoutInflater + .from(viewGroup.getContext()) + .inflate(id, viewGroup, attach); + } + public static void goneView(final View view, boolean isAnimated, boolean isSlow) { if (view == null) { return; @@ -40,6 +49,20 @@ public static void goneView(final View view, boolean isAnimated, boolean isSlow) } } + public static void goneViews(View... views) { + goneViews(true, views); + } + + public static void goneViews(boolean isAnimated, View... views) { + goneViews(isAnimated, true, views); + } + + public static void goneViews(boolean isAnimated, boolean isSlow, View... views) { + for (View view : views) { + goneView(view, isAnimated, isSlow); + } + } + public static void hideView(View view) { hideView(view, true); } @@ -67,6 +90,20 @@ public static void hideView(final View view, boolean isAnimated, boolean isSlow) } } + public static void hideViews(View... views) { + hideViews(true, views); + } + + public static void hideViews(boolean isAnimated, View... views) { + hideViews(isAnimated, true, views); + } + + public static void hideViews(boolean isAnimated, boolean isSlow, View... views) { + for (View view : views) { + hideView(view, isAnimated, isSlow); + } + } + public static void zoomOutView(final View view) { if (view == null) { return; @@ -128,6 +165,20 @@ public static void showView(final View view, boolean isAnimated, boolean isSlow) } } + public static void showViews(View... views) { + showViews(true, views); + } + + public static void showViews(boolean isAnimated, View... views) { + showViews(isAnimated, true, views); + } + + public static void showViews(boolean isAnimated, boolean isSlow, View... views) { + for (View view : views) { + showView(view, isAnimated, isSlow); + } + } + public static int blendColors(int color1, int color2, float amount, boolean inverse) { final byte ALPHA_CHANNEL = 24; final byte RED_CHANNEL = 16; @@ -235,5 +286,68 @@ public static void wave(final View layer, float scale, int duration, float stepO layer.startAnimation(scaleAnimation); } + public static void expandView(View v, int targetHeight, int initialHeight, After after) { + + Animation a = new ExpandAnimation(v, targetHeight, initialHeight); + + a.setDuration((targetHeight > initialHeight ? targetHeight : initialHeight / Screen.dp(1))); + a.setInterpolator(MaterialInterpolator.getInstance()); + v.clearAnimation(); + a.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + + } + + @Override + public void onAnimationEnd(Animation animation) { + after.doAfter(); + } + + @Override + public void onAnimationRepeat(Animation animation) { + + } + }); + v.startAnimation(a); + + } + + private static class ExpandAnimation extends Animation { + + private final View v; + private final int targetHeight; + private final int initialHeight; + private int currentHeight; + + public ExpandAnimation(View v, int targetHeight, int initialHeight) { + this.v = v; + this.targetHeight = targetHeight; + this.initialHeight = initialHeight; + this.currentHeight = initialHeight; + } + + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + if (targetHeight > initialHeight) { + currentHeight = + (int) ((targetHeight * interpolatedTime) - initialHeight * interpolatedTime + initialHeight); + } else { + currentHeight = + (int) (initialHeight - (initialHeight * interpolatedTime) - targetHeight * (1f - interpolatedTime) + targetHeight); + } + + v.getLayoutParams().height = currentHeight; + v.requestLayout(); + } + + @Override + public boolean willChangeBounds() { + return true; + } + } + public interface After { + void doAfter(); + } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/ActorToolbar.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/ActorToolbar.java new file mode 100644 index 0000000000..b7b0994d7d --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/ActorToolbar.java @@ -0,0 +1,142 @@ +package im.actor.sdk.view; + +import android.app.Activity; +import android.content.Context; +import android.graphics.ColorFilter; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.support.v7.view.menu.ActionMenuItemView; +import android.support.v7.widget.ActionMenuView; +import android.support.v7.widget.Toolbar; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AutoCompleteTextView; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import im.actor.sdk.view.avatar.AvatarView; + +public class ActorToolbar extends Toolbar { + + public ActorToolbar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + // TODO Auto-generated constructor stub + } + + public ActorToolbar(Context context, AttributeSet attrs) { + super(context, attrs); + // TODO Auto-generated constructor stub + } + + public ActorToolbar(Context context) { + super(context); + // TODO Auto-generated constructor stub + ctxt = context; + } + + int itemColor; + Context ctxt; + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + Log.d("LL", "onLayout"); + super.onLayout(changed, l, t, r, b); + colorizeToolbar(this, itemColor, (Activity) ctxt); + } + + public void setItemColor(int color) { + itemColor = color; + colorizeToolbar(this, itemColor, (Activity) ctxt); + } + + + /** + * Use this method to colorize toolbar icons to the desired target color + * + * @param toolbarView toolbar view being colored + * @param toolbarIconsColor the target color of toolbar icons + * @param activity reference to activity needed to register observers + */ + public static void colorizeToolbar(Toolbar toolbarView, int toolbarIconsColor, Activity activity) { + final PorterDuffColorFilter colorFilter + = new PorterDuffColorFilter(toolbarIconsColor, PorterDuff.Mode.SRC_IN); + + for (int i = 0; i < toolbarView.getChildCount(); i++) { + final View v = toolbarView.getChildAt(i); + + doColorizing(v, colorFilter, toolbarIconsColor); + } + + //Step 3: Changing the color of title and subtitle. + toolbarView.setTitleTextColor(toolbarIconsColor); + toolbarView.setSubtitleTextColor(toolbarIconsColor); + } + + public static void doColorizing(View v, final ColorFilter colorFilter, int toolbarIconsColor) { + if (v instanceof ImageButton) { + ((ImageButton) v).getDrawable().setAlpha(255); + ((ImageButton) v).getDrawable().setColorFilter(colorFilter); + } + + if (v instanceof ImageView && !(v instanceof AvatarView)) { + ((ImageView) v).getDrawable().setAlpha(255); + ((ImageView) v).getDrawable().setColorFilter(colorFilter); + } + + if (v instanceof AutoCompleteTextView) { + ((AutoCompleteTextView) v).setTextColor(toolbarIconsColor); + } + + if (v instanceof TextView) { + ((TextView) v).setTextColor(toolbarIconsColor); + } + + if (v instanceof EditText) { + ((EditText) v).setTextColor(toolbarIconsColor); + } + + if (v instanceof ViewGroup) { + for (int lli = 0; lli < ((ViewGroup) v).getChildCount(); lli++) { + doColorizing(((ViewGroup) v).getChildAt(lli), colorFilter, toolbarIconsColor); + } + } + + if (v instanceof ActionMenuView) { + for (int j = 0; j < ((ActionMenuView) v).getChildCount(); j++) { + + //Step 2: Changing the color of any ActionMenuViews - icons that + //are not back button, nor text, nor overflow menu icon. + final View innerView = ((ActionMenuView) v).getChildAt(j); + + if (innerView instanceof ActionMenuItemView) { + int drawablesCount = ((ActionMenuItemView) innerView).getCompoundDrawables().length; + for (int k = 0; k < drawablesCount; k++) { + if (((ActionMenuItemView) innerView).getCompoundDrawables()[k] != null) { + final int finalK = k; + + //Important to set the color filter in seperate thread, + //by adding it to the message queue + //Won't work otherwise. + //Works fine for my case but needs more testing + + ((ActionMenuItemView) innerView).getCompoundDrawables()[finalK].setColorFilter(colorFilter); + +// innerView.post(new Runnable() { +// @Override +// public void run() { +// ((ActionMenuItemView) innerView).getCompoundDrawables()[finalK].setColorFilter(colorFilter); +// } +// }); + } + } + } + } + } + } + + +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/BlockView.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/BlockView.java new file mode 100644 index 0000000000..0d6c6e5aa1 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/BlockView.java @@ -0,0 +1,60 @@ +package im.actor.sdk.view; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.AttributeSet; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import im.actor.sdk.R; +import im.actor.sdk.util.Screen; + +public class BlockView extends LinearLayout { + + private Drawable topDrawable; + private Drawable bottomDrawable; + + public BlockView(Context context) { + super(context); + init(); + } + + public BlockView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public BlockView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public BlockView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + private void init() { + setPadding(0, Screen.dp(8), 0, Screen.dp(8)); + setWillNotDraw(false); + + bottomDrawable = getResources().getDrawable(R.drawable.card_shadow_bottom); + topDrawable = getResources().getDrawable(R.drawable.card_shadow_top); + } + + @Override + protected void onDraw(Canvas canvas) { + + bottomDrawable.setBounds(0, getHeight() - Screen.dp(8), getWidth(), getHeight() - Screen.dp(4)); + bottomDrawable.draw(canvas); + + topDrawable.setBounds(0, Screen.dp(7), getWidth(), Screen.dp(8)); + topDrawable.draw(canvas); + + super.onDraw(canvas); + } +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/SearchHighlight.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/SearchHighlight.java index c752449112..75a5e0bacb 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/SearchHighlight.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/SearchHighlight.java @@ -13,7 +13,7 @@ public class SearchHighlight { public static Spannable highlightQuery(String src, String query, int color) { - String matchString = src.toLowerCase(); + String matchString = src.toLowerCase().replace("@", " "); SpannableStringBuilder builder = new SpannableStringBuilder(src); if (matchString.startsWith(query)) { builder.setSpan(new ForegroundColorSpan(color), 0, query.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/adapters/BottomSheetListView.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/adapters/BottomSheetListView.java new file mode 100644 index 0000000000..03bbadf7f8 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/adapters/BottomSheetListView.java @@ -0,0 +1,144 @@ +package im.actor.sdk.view.adapters; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import im.actor.sdk.ActorSDK; +import im.actor.sdk.R; +import im.actor.sdk.util.Screen; + +public class BottomSheetListView extends RecyclerListView { + + private FrameLayout header; + private View underlyingView; + private int minHeight = 0; + + public BottomSheetListView(Context context) { + super(context); + init(); + } + + public BottomSheetListView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public BottomSheetListView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public BottomSheetListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + + @Override + public int getCount() { + return super.getCount() - 1; + } + + private void init() { + setSelector(new ColorDrawable(Color.TRANSPARENT)); + setOverScrollMode(OVER_SCROLL_NEVER); + setVerticalScrollBarEnabled(false); + header = new FrameLayout(getContext()); +// header.setBackgroundColor(ActorSDK.sharedActor().style.getAccentColor()); + header.setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0)); + ImageView shadow = new ImageView(getContext()); + shadow.setScaleType(ImageView.ScaleType.FIT_XY); + shadow.setImageResource(R.drawable.conv_field_shadow); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, Screen.dp(2), Gravity.BOTTOM); + header.addView(shadow, params); + addHeaderView(header); + + setOnTouchListener(new View.OnTouchListener() { + boolean delegateTouch = false; + + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + boolean stopDelegate = false; + switch (motionEvent.getAction()) { + case MotionEvent.ACTION_DOWN: + delegateTouch = isWithinHeaderBounds(motionEvent.getRawX(), motionEvent.getRawY()); + break; + + case MotionEvent.ACTION_UP: + stopDelegate = true; + } + + if (delegateTouch && underlyingView != null) { + delegateTouch = !stopDelegate; + underlyingView.dispatchTouchEvent(motionEvent); + return true; + } + + + return false; + } + }); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + resizeHeader(); + } + + protected void resizeHeader() { + setVisibility(minHeight == 0 ? GONE : VISIBLE); + if (header.getLayoutParams().height != getHeight() - minHeight) { + header.getLayoutParams().height = getHeight() - minHeight; + header.requestLayout(); + } + } + + public void setMinHeight(int minHeight) { + this.minHeight = minHeight; + resizeHeader(); + } + + public View getHeader() { + return header; + } + + boolean isWithinHeaderBounds(float xPoint, float yPoint) { + int[] l = new int[2]; + header.getLocationOnScreen(l); + int x = l[0]; + int y = l[1]; + int w = header.getWidth(); + int h = header.getHeight(); + + if (xPoint < x || xPoint > x + w || yPoint < y || yPoint > y + h) { + return false; + } + return true; + } + + public void setUnderlyingView(View underlyingView) { + this.underlyingView = underlyingView; + } + + @Override + public void setOnItemClickListener(OnItemClickListener listener) { + super.setOnItemClickListener((adapterView, view, i, l) -> listener.onItemClick(adapterView, view, i - 1, l)); + } + + @Override + public void setOnItemLongClickListener(OnItemLongClickListener listener) { + super.setOnItemLongClickListener((adapterView, view, i, l) -> listener.onItemLongClick(adapterView, view, i - 1, l)); + } +} diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/avatar/AvatarPlaceholderDrawable.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/avatar/AvatarPlaceholderDrawable.java index 8c59427a92..225e37bc48 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/avatar/AvatarPlaceholderDrawable.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/avatar/AvatarPlaceholderDrawable.java @@ -10,6 +10,7 @@ import android.text.TextPaint; import android.util.TypedValue; +import im.actor.sdk.ActorSDK; import im.actor.sdk.R; import im.actor.sdk.util.Fonts; @@ -19,7 +20,6 @@ public class AvatarPlaceholderDrawable extends Drawable { private static float TEXT_SIZE; private float selfTextSize; private static Paint CIRCLE_PAINT; - private static int[] COLORS; private Context ctx; private String title; @@ -47,17 +47,7 @@ public AvatarPlaceholderDrawable(String title, int id, float selfTextSize, Conte } } - if (COLORS == null) { - COLORS = new int[]{ - context.getResources().getColor(R.color.placeholder_0), - context.getResources().getColor(R.color.placeholder_1), - context.getResources().getColor(R.color.placeholder_2), - context.getResources().getColor(R.color.placeholder_3), - context.getResources().getColor(R.color.placeholder_4), - context.getResources().getColor(R.color.placeholder_5), - context.getResources().getColor(R.color.placeholder_6), - }; - } + int[] colors = ActorSDK.sharedActor().style.getDefaultAvatarPlaceholders(); if (CIRCLE_PAINT == null) { CIRCLE_PAINT = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -74,7 +64,7 @@ public AvatarPlaceholderDrawable(String title, int id, float selfTextSize, Conte if (id == 0) { this.color = context.getResources().getColor(R.color.placeholder_empty); } else { - this.color = COLORS[Math.abs(id) % COLORS.length]; + this.color = colors[Math.abs(id) % colors.length]; } this.title = title; diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/avatar/AvatarView.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/avatar/AvatarView.java index 48c19a4e1a..2f97d56a45 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/avatar/AvatarView.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/avatar/AvatarView.java @@ -99,6 +99,9 @@ public void bind(PublicGroup group) { bind(group.getAvatar(), group.getTitle(), group.getId()); } + public void updatePlaceholder(String title, int id) { + getHierarchy().setPlaceholderImage(new AvatarPlaceholderDrawable(title, id, placeholderTextSize, getContext())); + } public void bind(Avatar avatar, String title, int id) { // Same avatar diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/emoji/keyboard/BaseKeyboard.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/emoji/keyboard/BaseKeyboard.java index b9364a3ab8..1466964b30 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/emoji/keyboard/BaseKeyboard.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/emoji/keyboard/BaseKeyboard.java @@ -2,23 +2,28 @@ import android.app.Activity; import android.content.Context; -import android.content.Intent; import android.content.res.Configuration; import android.graphics.PixelFormat; import android.graphics.Rect; -import android.net.Uri; import android.os.Build; -import android.provider.Settings; import android.view.Gravity; import android.view.View; +import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.RelativeLayout; import android.widget.TextView; +import java.lang.reflect.Field; + import im.actor.sdk.R; import im.actor.runtime.Log; +import im.actor.sdk.controllers.conversation.KeyboardLayout; +import im.actor.sdk.util.KeyboardHelper; +import im.actor.sdk.util.Screen; public class BaseKeyboard implements ViewTreeObserver.OnGlobalLayoutListener { @@ -28,25 +33,31 @@ public class BaseKeyboard implements protected Activity activity; private View decorView; private boolean softKeyboardListeningEnabled = true; + private boolean showRequested = false; private boolean emojiKeyboardIsOpening; private InputMethodManager inputMethodManager; private View emojiKeyboardView; protected EditText messageBody; + protected KeyboardLayout root; + protected RelativeLayout container; public static final int OVERLAY_PERMISSION_REQ_CODE = 735; Boolean pendingOpen = false; - private KeyboardStatusListener keyboardStatusListener; + protected KeyboardStatusListener keyboardStatusListener; final WindowManager windowManager; - int keyboardHeight = 0; + int keyboardHeight; private boolean showingPending; - private boolean showing; - private boolean dismissed; + private boolean showing = false; + // private boolean dismissed; private boolean softwareKeyboardShowing; + private KeyboardHelper keyboardHelper; + private boolean keyboardMeasured = false; + private int keyboardTriggerHeight = Screen.dp(150); - public BaseKeyboard(Activity activity) { + public BaseKeyboard(Activity activity, EditText messageBody) { this.activity = activity; this.windowManager = activity.getWindowManager(); this.inputMethodManager = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); @@ -55,6 +66,20 @@ public BaseKeyboard(Activity activity) { //setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); //default size keyboardHeight = (int) activity.getResources().getDimension(R.dimen.keyboard_height); + keyboardHelper = new KeyboardHelper(activity); + this.messageBody = messageBody; + + messageBody.setOnClickListener(view -> { + if (showing) { + dismiss(); + } + }); + messageBody.setOnFocusChangeListener((view, b) -> { + if (b && showing) { + dismiss(); + } + }); + } @@ -67,88 +92,45 @@ public void setKeyboardStatusListener(KeyboardStatusListener keyboardStatusListe } - public void show(EditText messageBody) { - this.messageBody = messageBody; - - showing = true; - dismissed = false; - if (softwareKeyboardShowing) { - showInternal(); - } else { - messageBody.setFocusableInTouchMode(true); - messageBody.requestFocus(); - inputMethodManager.showSoftInput(messageBody, InputMethodManager.SHOW_IMPLICIT); + public void show() { + if (isShowing()) { + return; } - } + softKeyboardListeningEnabled = true; + this.root = (KeyboardLayout) messageBody.getRootView().findViewById(R.id.container).getParent(); + this.container = (RelativeLayout) messageBody.getRootView().findViewById(R.id.container); - private void showInternal() { - //Check - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (!Settings.canDrawOverlays(activity)) { - Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, - Uri.parse("package:" + activity.getPackageName())); - activity.startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE); - } else { - showChecked(); - } + root.showInternal(keyboardHeight); + showRequested = true; + if (softwareKeyboardShowing) { + keyboardHelper.setImeVisibility(messageBody, false); } else { - showChecked(); +// messageBody.setFocusableInTouchMode(true); +// messageBody.requestFocus(); +// inputMethodManager.showSoftInput(messageBody, InputMethodManager.SHOW_IMPLICIT); + container.setPadding(0, 0, 0, keyboardHeight); + showInternal(); } } - public void showChecked() { - if (showing == (emojiKeyboardView != null)) { + protected void showInternal() { + if (isShowing()) { return; } + + showRequested = false; + showing = true; + emojiKeyboardView = createView(); - WindowManager.LayoutParams params = new WindowManager.LayoutParams( - WindowManager.LayoutParams.MATCH_PARENT, - (keyboardHeight), - WindowManager.LayoutParams.TYPE_PHONE, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSLUCENT); - - params.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; - windowManager.addView(emojiKeyboardView, params); -// emojiKeyboardView.post(new Runnable() { -// @Override -// public void run() { -// AlphaAnimation animation = new AlphaAnimation(0, 1); -// animation.setDuration(400); -// animation.setInterpolator(new MaterialInterpolator()); -// animation.setStartOffset(0); -// animation.setAnimationListener(new Animation.AnimationListener() { -// @Override -// public void onAnimationStart(Animation animation) { -// Log.d(TAG, "onAnimationStart"); -// } -// -// @Override -// public void onAnimationEnd(Animation animation) { -// Log.d(TAG, "onAnimationEnd"); -// } -// -// @Override -// public void onAnimationRepeat(Animation animation) { -// Log.d(TAG, "onAnimationReset"); -// } -// }); -// } -// }); - -// emojiKeyboardView.setTranslationY(140); -// emojiKeyboardView -// .animate() -// .y(0) -// .setDuration(200) -// .setStartDelay(0) -// .setInterpolator(new DecelerateInterpolator(1.4f)) -// .start(); - - if (keyboardStatusListener != null) + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, keyboardHeight); + params.gravity = Gravity.BOTTOM; + root.addView(emojiKeyboardView, params); + + if (keyboardStatusListener != null) { keyboardStatusListener.onShow(); + } onShow(); } @@ -168,13 +150,26 @@ private void update() { } public void dismiss() { - dismissed = true; - showing = false; - dismissInternally(); + dismissInternally(false); + } + + public void dismiss(boolean dismissAll) { + dismissInternally(dismissAll); } - private void dismissInternally() { - if (dismissed && emojiKeyboardView != null) { + private void dismissInternally(boolean dismissAll) { + showing = false; + if (messageBody != null) { + if (dismissAll) { + keyboardHelper.setImeVisibility(messageBody, false); + } else if (!softwareKeyboardShowing) { + keyboardHelper.setImeVisibility(messageBody, true); + } + } + if (root != null) { + root.dismissInternal(); + } + if (emojiKeyboardView != null && root != null && keyboardHelper != null) { final View emojiKeyboardViewCopy = emojiKeyboardView; // emojiKeyboardView // .animate() @@ -192,36 +187,35 @@ private void dismissInternally() { // }) // .start(); emojiKeyboardViewCopy.setVisibility(View.GONE); - windowManager.removeView(emojiKeyboardViewCopy); - showing = false; + root.removeView(emojiKeyboardViewCopy); emojiKeyboardView = null; - if (keyboardStatusListener != null) + if (keyboardStatusListener != null) { keyboardStatusListener.onDismiss(); + } + if (dismissAll) { + container.setPadding(0, 0, 0, 0); + } onDismiss(); } } - public void toggle(EditText messageBody) { + public void toggle() { if (isShowing()) { dismiss(); } else { - show(messageBody); + show(); } } public boolean isShowing() { - return emojiKeyboardView != null; + return showing && emojiKeyboardView != null; } public void destroy() { - showing = false; - dismissed = true; - if (emojiKeyboardView != null) { - windowManager.removeView(emojiKeyboardView); - emojiKeyboardView = null; - } + dismiss(true); + if (keyboardStatusListener != null) { keyboardStatusListener.onDismiss(); } @@ -229,56 +223,87 @@ public void destroy() { @Override public void onGlobalLayout() { + Log.d(TAG, "onGlobalLayout"); if (!softKeyboardListeningEnabled) { return; } Rect r = new Rect(); - decorView.getWindowVisibleDisplayFrame(r); + messageBody.getWindowVisibleDisplayFrame(r); - int screenHeight = decorView.getRootView() + int screenHeight = messageBody.getRootView() .getHeight(); - int heightDifference = screenHeight - - (r.bottom - r.top); + + +// int widthDiff = decorView.getRootView().getWidth() - (r.right - r.left); +// if (Math.abs(widthDiff) > 0) { +// return; +// } int resourceId = activity.getResources() .getIdentifier("status_bar_height", "dimen", "android"); + int statusBarHeight = 0; + if (resourceId > 0) { - heightDifference -= activity.getResources() + statusBarHeight = activity.getResources() .getDimensionPixelSize(resourceId); - } - int orientation = activity.getResources().getConfiguration().orientation; - - int id = activity.getResources().getIdentifier("config_showNavigationBar", "bool", "android"); - if (id > 0) { - if (activity.getResources().getBoolean(id)) { - int navbarResId = activity.getResources() - .getIdentifier( - orientation == Configuration.ORIENTATION_PORTRAIT ? "navigation_bar_height" : "navigation_bar_height_landscape", - "dimen", "android"); - if (navbarResId > 0) { - heightDifference -= activity.getResources() - .getDimensionPixelSize(navbarResId); - } - } + screenHeight -= statusBarHeight; } - if (heightDifference > 100) { - Log.d(TAG, "onGlobalLayout: " + heightDifference); + + screenHeight -= getViewInset(root, statusBarHeight); + + int heightDifference = screenHeight - (r.bottom - r.top); + + boolean changed = softwareKeyboardShowing; + + if (heightDifference > keyboardTriggerHeight) { + softwareKeyboardShowing = true; + + Log.d(TAG, "onGlobalLayout: " + heightDifference); + keyboardHeight = heightDifference; - Log.d(TAG, "onGlobalLayout: " + "showing"); - showInternal(); + dismiss(); + } else { + + softwareKeyboardShowing = false; + + if (showRequested) { + root.showInternal(keyboardHeight); + showInternal(); + } else if (changed) { + if (root != null) { + root.dismissInternal(); + } + } Log.d(TAG, "onGlobalLayout: " + heightDifference); Log.d(TAG, "onGlobalLayout: " + "dismiss?"); // dismiss not wirk - softwareKeyboardShowing = false; +// softwareKeyboardShowing = false; // keyboard showing or not? - dismissed = true; - dismissInternally(); +// dismissed = true; +// dismissInternally(); } + + changed = changed != softwareKeyboardShowing; + Log.d(TAG, "keyboard state change: " + changed); + + // FIXME verify root view applied new padding after keyboard state change + // workaround for [some of android versions] bug, when keyboard closing not causing relayout, or causing it with delay + if (changed && root != null) { + + root.postDelayed(() -> { + if (!root.isSync()) { + root.requestLayout(); + } + }, 30); + } + + + } public Activity getActivity() { @@ -301,4 +326,44 @@ protected View createView() { return view; } + + public void onConfigurationChange() { +// dismiss(true); +// softwareKeyboardShowing = false; + } + + public boolean onBackPressed() { + if (showing) { + dismiss(true); + return true; + } + return false; + } + + public static int getViewInset(View view, int statusBarHeight) { + if (view == null || view.getRootView() == null) { + return 0; + } + + view = view.getRootView(); + + if (Build.VERSION.SDK_INT < 21 || view.getHeight() == Screen.getHeight() || view.getHeight() == Screen.getHeight() - statusBarHeight) { + return 0; + } + + try { + Field mAttachInfoField = View.class.getDeclaredField("mAttachInfo"); + mAttachInfoField.setAccessible(true); + Object mAttachInfo = mAttachInfoField.get(view); + if (mAttachInfo != null) { + Field mStableInsetsField = mAttachInfo.getClass().getDeclaredField("mStableInsets"); + mStableInsetsField.setAccessible(true); + Rect insets = (Rect) mStableInsetsField.get(mAttachInfo); + return insets.bottom; + } + } catch (Exception e) { + e.printStackTrace(); + } + return 0; + } } diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/emoji/keyboard/emoji/EmojiKeyboard.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/emoji/keyboard/emoji/EmojiKeyboard.java index 0e10cfaf04..ccfb642faf 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/emoji/keyboard/emoji/EmojiKeyboard.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/emoji/keyboard/emoji/EmojiKeyboard.java @@ -23,6 +23,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; +import android.widget.EditText; import android.widget.FrameLayout; import android.widget.LinearLayout; @@ -54,8 +55,8 @@ public class EmojiKeyboard extends BaseKeyboard implements OnSmileClickListener, private SmilePagerAdapter mEmojisAdapter; - public EmojiKeyboard(Activity activity) { - super(activity); + public EmojiKeyboard(Activity activity, EditText messageBody) { + super(activity, messageBody); } @Override diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/markdown/AndroidMarkdown.java b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/markdown/AndroidMarkdown.java index 18119b7744..5e5bfb8257 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/markdown/AndroidMarkdown.java +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/java/im/actor/sdk/view/markdown/AndroidMarkdown.java @@ -24,9 +24,11 @@ import android.view.View; import android.widget.Toast; +import im.actor.core.entity.Peer; import im.actor.runtime.actors.ActorContext; import im.actor.sdk.ActorSDK; import im.actor.sdk.R; +import im.actor.sdk.controllers.activity.BaseActivity; import im.actor.sdk.controllers.conversation.ChatActivity; import im.actor.sdk.controllers.fragment.preview.CodePreviewActivity; import im.actor.runtime.android.AndroidContext; @@ -119,11 +121,8 @@ private static void writeText(MDText[] texts, SpannableStringBuilder builder) { @Override public void onClick(View view) { Context ctx = view.getContext(); - if (url.getUrl().startsWith("send:")) { - ctx = extractContext(ctx); -// if (ctx instanceof ChatActivity) { -// ActorSDK.sharedActor().getMessenger().sendMessage(((ChatActivity) ctx).getPeer(), url.getUrl().replace("send:", "")); -// } + if (url.getUrl().startsWith("send:") && view.getTag(R.id.peer) != null && view.getTag(R.id.peer) instanceof Peer) { + ActorSDK.sharedActor().getMessenger().sendMessage((Peer) view.getTag(R.id.peer), url.getUrl().replaceFirst("send:", "")); } else { Intent intent = buildChromeIntent().intent; intent.setData(Uri.parse(url.getUrl())); @@ -149,16 +148,6 @@ public void onClick(View view) { } } - private static Context extractContext(Context ctx) { - if (ctx instanceof AppCompatActivity) { - return ctx; - } else if (ctx instanceof ContextWrapper) { - return extractContext(((ContextWrapper) ctx).getBaseContext()); - } - - return ctx; - } - public static CustomTabsIntent buildChromeIntent() { CustomTabsIntent.Builder customTabsIntent = new CustomTabsIntent.Builder(); diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res-material/layout/share_menu.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res-material/layout/share_menu.xml index 2daec0fac1..6015f16674 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res-material/layout/share_menu.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res-material/layout/share_menu.xml @@ -7,21 +7,15 @@ android:id="@+id/cancelField" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/dialog_overlay" android:clickable="true" /> - - - + + + + + + + + \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/activity_root.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/activity_root.xml index 89a0a9b26e..adf9a08d9e 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/activity_root.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/activity_root.xml @@ -10,6 +10,12 @@ android:layout_height="match_parent" android:orientation="vertical"> + + - - + android:layout_height="match_parent" /> \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/activity_root_content.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/activity_root_content.xml index 7e59728433..f3b0be42e4 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/activity_root_content.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/activity_root_content.xml @@ -7,24 +7,20 @@ + android:layout_height="match_parent" /> + android:layout_height="match_parent" /> + android:layout_height="match_parent" /> + android:layout_height="match_parent" /> diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/actor_settings_field.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/actor_settings_field.xml index faaf8db974..26083c93a5 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/actor_settings_field.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/actor_settings_field.xml @@ -12,10 +12,8 @@ + android:layout_width="72dp" + android:layout_height="match_parent" /> + android:layout_height="match_parent" /> - - - - - - - + \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fr_settings_chat.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fr_settings_chat.xml index 16cc2216c6..26733a58a9 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fr_settings_chat.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fr_settings_chat.xml @@ -107,6 +107,167 @@ android:gravity="center_vertical" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + - - + android:layout_height="@dimen/div_size"/> - + android:layout_height="@dimen/div_size"/> - + android:layout_height="@dimen/div_size"/> - + android:layout_height="@dimen/div_size"/> - - + android:layout_height="@dimen/div_size"/> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_chat.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_chat.xml index 07fda5a107..367533acbb 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_chat.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_chat.xml @@ -1,68 +1,73 @@ - - + android:layout_height="match_parent"> - - - + - + - + - + + android:layout_height="wrap_content" + android:layout_above="@+id/sendContainer" + android:visibility="gone" /> + android:layout_height="wrap_content" + android:layout_alignParentBottom="true"> - + + - - + android:visibility="gone"> - \ No newline at end of file + + + + + diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_chat_mention_item.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_chat_mention_item.xml index 8e68a6da17..952c25d8cb 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_chat_mention_item.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_chat_mention_item.xml @@ -10,6 +10,7 @@ android:orientation="vertical"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_edit_info.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_edit_info.xml new file mode 100644 index 0000000000..2f8116d260 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_edit_info.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_edit_permissions.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_edit_permissions.xml new file mode 100644 index 0000000000..6e8b2d9ca8 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_edit_permissions.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_edit_type.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_edit_type.xml new file mode 100644 index 0000000000..e13fcd6d31 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_edit_type.xml @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_group.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_group.xml index f42d230b31..ce95a72838 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_group.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_group.xml @@ -30,10 +30,4 @@ android:text="@string/group_not_member" android:textSize="21sp" /> - - \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_group_header.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_group_header.xml index da08f4c975..00d92519b9 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_group_header.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_group_header.xml @@ -10,160 +10,133 @@ android:layout_height="wrap_content" android:orientation="vertical"> - - - + android:layout_height="148dp" + android:layout_gravity="bottom" + android:gravity="center_vertical" + android:paddingTop="44dp"> - + android:layout_width="52dp" + android:layout_height="52dp" + android:layout_gravity="center_vertical" + android:layout_margin="18dp" /> + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_weight="1" + android:orientation="vertical"> - - + android:textSize="20sp" /> + android:gravity="left" + android:textSize="16sp" /> + - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="wrap_content" + android:layout_marginBottom="12dp" + android:gravity="top" + android:minHeight="48dp"> + + + android:gravity="center_vertical" + android:minHeight="48dp" + android:paddingBottom="8dp" + android:paddingLeft="72dp" + android:paddingRight="8dp" + android:paddingTop="8dp" + android:textSize="16sp" /> - - - - - + + + + + + + + + + - - + android:layout_height="wrap_content" + android:layout_marginLeft="72dp" + android:layout_marginTop="4dp" + android:background="@color/picker_list_divider" /> - - - - + app:src="@drawable/ic_menu_black_18dp" /> + + + + + + + + + android:layout_height="8dp" + android:layout_marginTop="12dp"> - - - - - - - - - - \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_group_item.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_group_item.xml index d9b18e2b1b..f13caa7823 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_group_item.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_group_item.xml @@ -29,10 +29,10 @@ + android:orientation="vertical"> + - + + + + + \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_members.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_members.xml new file mode 100644 index 0000000000..2d6d38c795 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_members.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_profile.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_profile.xml index dd2a761e8d..8ffa7e6ca8 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_profile.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_profile.xml @@ -9,217 +9,301 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - + android:layout_height="wrap_content"> - - - - + + - - + + + + + + + + + + + + + android:orientation="vertical" /> - - - + + android:gravity="center_vertical" + android:paddingRight="8dp"> + android:id="@+id/newMessageIcon" + android:layout_width="72dp" + android:layout_height="72dp" + android:scaleType="center" /> + android:id="@+id/newMessageText" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:gravity="center_vertical" + android:text="@string/profile_compose" + android:textSize="16sp" /> + + android:gravity="center_vertical" + android:paddingRight="8dp"> + android:id="@+id/videoCallIcon" + android:layout_width="72dp" + android:layout_height="72dp" + android:scaleType="center" /> + android:id="@+id/videoCallText" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:gravity="center_vertical" + android:text="@string/profile_video_call" + android:textSize="16sp" /> + + + + android:gravity="center_vertical" + android:paddingRight="8dp"> + android:layout_width="72dp" + android:layout_height="72dp" + android:scaleType="center" /> + android:textSize="16sp" /> + - + - + - + - - - - - + + + + + + + + + + + + + + + + + + + + + + android:paddingRight="8dp" + android:layout_marginBottom="8dp"> - + + + + + - + android:layout_height="100dp" + android:layout_gravity="top|right"> + + + - - - - - - - + + \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_settings.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_settings.xml index 3a58f6a61e..b43486a427 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_settings.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/layout/fragment_settings.xml @@ -81,10 +81,9 @@ + android:layout_width="72dp" + android:scaleType="centerInside" + android:layout_height="match_parent" /> \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/menu/chat_menu.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/menu/chat_menu.xml index 500ad7ad93..5a8e39faa5 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/menu/chat_menu.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/menu/chat_menu.xml @@ -26,16 +26,16 @@ android:title="" app:showAsAction="always" /> - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/menu/group_info.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/menu/group_info.xml index 777674e5c7..e5f1d283ff 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/menu/group_info.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/menu/group_info.xml @@ -4,24 +4,11 @@ ~ Copyright (C) 2015 Actor LLC. --> - + + android:id="@+id/edit" + android:icon="@drawable/ic_edit_white_36dp" + android:title="@string/actor_menu_edit" /> - - - - - \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/menu/sign_in.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/menu/sign_in.xml index 20ba1e8aa5..dc7c72f8f6 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/menu/sign_in.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/menu/sign_in.xml @@ -12,4 +12,9 @@ android:title="@string/tour_sign_up" app:showAsAction="always" /> + + \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/menu/sign_up.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/menu/sign_up.xml index f14405c8f6..154df3d205 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/menu/sign_up.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/menu/sign_up.xml @@ -12,4 +12,9 @@ android:title="@string/tour_sign_in" app:showAsAction="always" /> + + \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-ar/ui_text.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-ar/ui_text.xml index bfbce0ad36..99ca76257d 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-ar/ui_text.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-ar/ui_text.xml @@ -269,7 +269,7 @@ صورة صورة المجموعة لا يوجد صورة - تعديل + تعديل ضع كـ… diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-fa/ui_text.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-fa/ui_text.xml index 87ebe8cdfb..643c8f71d8 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-fa/ui_text.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-fa/ui_text.xml @@ -345,7 +345,7 @@ نگاره نگاره گروه بدون نگاره - ویرایش + ویرایش تنظیم به عنوان... diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-nl/ui_text.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-nl/ui_text.xml new file mode 100644 index 0000000000..79d53e45b9 --- /dev/null +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-nl/ui_text.xml @@ -0,0 +1,649 @@ + + + + Homecrew + Nl + + Bevestig + Annuleer + Ja + Nee + Probeer opnieuw + + Neem foto + Kies een foto uit de gallerij + Verwijder foto + Ophalen + + Laden… + + Geen SD-kaart gevonden + Telefoonnummer gekopieerd naar klembord + E-mailadres gekopieerd naar klembord + Berichten gekopieerd naar klembord + Gebruikersnaam gekopieerd naar klembord + Kan gebruiker niet uit de groep verwijderen + Kan gebruiker niet aan de groep toevoegen + Gebruiker is al lid van de groep + "Naam" veld mag niet leeg zijn + Naam wijzigen niet geslaagd + "Gebruikersnaam" sectie mag niet leeg zijn + Gebruikersnaam wijzigen niet geslaagd + "Over mij" veld mag niet leeg zijn + Geen applicatie beschikbaar om dit te openen + Kan de groep niet verlaten + Kan de groep niet aanmaken + Kan het gesprek niet verwijderen + Kan het gesprek niet wissen + + Delen + Opslaan naar telefoon + Opgeslagen + Volgende + + + Hoi! Zullen we {appName} gebruiken? Handig voor een veilige en verbonden buurt! {inviteUrl} + Via SMS uitnodigen + Via e-mail uitnodigen + + + REGISTREREN + AANMELDEN + met telefoonnummer + met e-mail + met Google + Naar boven + + Homecrew + + Voorkom inbraak. Buren worden dienstverleners. + + Heel de buurt in een chat + Onze buurtchat ondersteunt meer dan 300 gebruikers in een enkele groep. Communiceer met je buurtgenoten en meld elkaar bijzonderheden. + + Werkt overal + Waar je ook bent in de buurt, thuis of buiten, berichten komen aan bij andere buurtgenoten. + + Beveiligd & besloten + Laat berichten achter voor contactpersonen of in de afgesloten buurtchat. Deel documenten, bestanden en foto\'s zonder zorgen dat ze in de verkeerde handen vallen. Alle gesprekken zijn met encryptie versleuteld. + + + Aanmelden + Selecteer de landcode en voer het telefoonnummer in. + Telefoonnummer + BEVESTIG HET NUMMER + WAAROM HEBBEN WE HET NUMMER NODIG? + We gebruiken dit telefoonnummer voor authenticatie, dit is een veilige manier van aanmelden.\n\nWachtwoorden zijn veelal te makkelijk om te raden en e-mail kan eenvoudig in de verkeerde handen vallen. + Hebbes! + Selecteer een land + Ongeldige landcode + + We gebruiken het e-mailadres om contactpersonen te zoeken en om eventueel automatisch aan groepen toewijzen. + WAAROM HEBBEN WIJ HET E-MAILADRES NODIG? + + Gebruik e-mail om te registreren + Gebruik telefoon om te registreren + + + Kies een soort authorisatie + Kies a.u.b. een authorisatiemethode. + Registeren + Aanmelden + met telefoonnummer + met e-mailadres + + + + Registreren + Geef het e-mailadres op. + E-mail + BEVESTIG E-MAIL + + + Bij gebruik van deze dienst wordt akkoord gegaan met de algemene voorwaarden en het privacybeleid. + Bij gebruik van deze dienst wordt met de algemene voorwaarden akkoord gegaan. + Bij gebruik van deze dienst wordt met het privacybeleid akkoord gegaan. + Andere gebruikers zijn in staat deze account te vinden op basis van telefoonnummer of gebruikersnaam. + + Wijzig server end-point + fabrieksinstellingen herstellen + + Algemene Voorwaarden + Privacybeleid + + + Aanmelden + Voer een telefoonnummer of e-mailadres in. + Geef een telefoonnummer op. + Geef een e-mailadres. + telefoonnummer of e-mailadres + telefoon + e-mail + AANMELDEN + + + We hebben een SMS met de activatiecode naar het telefoonnummer {0} gestuurd + We hebben een e-mail met de activatiecode naar {0} gestuurd + Voer de SMS code in + Code + KLAAR + VERKEERD TELEFOONNUMMER? + VERKEERDE E-MAIL? + Wijzig dit nummer? + Wijzig dit e-mail? + Wijzigen + + + Hoi! Vertel ons je naam + Vertel ons je naam + Je naam + VOLGENDE + + + Geef een geldig telefoonnummer of e-mailadres op + Geef een telefoonnummer op + Geef een e-mailadres op + De code is verlopen. Voer validatieproces opnieuw uit. + De code is ongeldig. Probeer het nog eens. + Authorisatie serverfout. Probeer het nog eens. + + + Typ iets om te zoeken + Homecrew doorzoeken + Niets gevonden + Help + Zoeken + + GESPREKKEN + CONTACTEN + + Opstellen + Maak groep + Maak kanaal + Lid worden van openbare groep + Contactpersoon toevoegen + + Bezig met synchroniseren + Een ogenblik geduld, we zijn de account aan het inrichten. + + Nodig buurtgenoten uit + Geen van de bekende contactpersonen gebruiken {appName}. Gebruik de knop beneden om ze uit te nodigen. + VERTEL EEN VRIEND + of voeg handmatig een contact toe + CONTACT TOEVOEGEN + + + Verstuur mijn contactpersoon… + Mijn telefoonnummer is {0} + Bewerk profiel + Wijzig foto + Account + Over Homecrew + Help + Stel een vraag of geef feedback + Twitter + Home + Meldingen + Gespreksinstellingen + Beveiliging en privacy + Lijst van geblokkeerde contacten + Stel gebruikersnaam in + Wijzig gebruikersnaam + Gebruikersnaam + In {appName} kan een gebruikersnaam gebruikt worden. Anderen zijn dan in staat deze account te vinden door hierop te zoeken en contact op te nemen zonder een telefoonnummer te gebruiken.\n\nHet is toegestaan de (hoofd)letters a–z, de getallen 0–9 en liggende streepjes te gebruiken. De minimum lengte is 5 tekens. + Gebruikersnaam invoeren + Over mij + Over gebruiker + Achtergrondafbeelding + + + Bel {0} + E-mail {0} + SMS {0} + Deel {0} + Kopie + + + Gesprekken + Lang ingedrukt houden voor meer opties + Begin een gesprek + Selecteer een contact uit de lijst of gebruik het pennetje om een conversatie te beginnen. + + Bekijk contactpersoon + Hernoem contactpersoon + Verwijder conversatie + + Bekijk groepsinformatie + Hernoem groep + Verlaat groep + Verwijder groep + + Bekijk informatie over het kanaal + Hernoem kanaal + Verlaat kanaal + Verwijder kanaal + + + Contacten + Geen bestaande contacten gevonden + Vertel buurtgenoten en anderen over {appName} + Voeg iemand toe… + + Nodig buurtgenoten uit + Geen van de bestaande contacten gebruiken {appName}. Gebruik de knop hieronder om ze uit te nodigen. + Vertel iemand over ons + of voeg handmatig contacten toe met de plus knop hieronder. + + Verwijder {0} uit mijn contacten + Verwijderen… + Bewerk naam + + Uitnodigen via link + + Alles selecteren + Alles deselecteren + + Deel de link + Stuur de link via SMS/e-mail naar meerdere contacten + + + Groepsinformatie + Bekijk contactinformatie + Bellen + Bestanden + Gespreksgeschiedenis wissen + Groep verlaten + + Plaatsen + Contact + Bestand + Video opnemen + Foto maken + Gallerij + + Het bericht… + + Nieuwe berichten + Aan contacten toevoegen + Contact toevoegen… + + Openen + Verzenden + Stoppen + Downloaden + VEEG OM TE ANNULEREN + + Geen groepslidmaatschap gevonden + Groep verwijderd + Kanaal verwijderd + + Lid worden + Microfoon uit + Microfoon aan + + START + Nog geen beschrijving van deze bot + + Het bericht wordt nog niet ondersteund in deze versie. Even geduld tot de applicatie bijgewerkt is om het bericht te zien. + \u25CF online + + Doorsturen + Citeren + Verwijderen + Kopieer + Vind ik leuk + Bewerk laatste bericht + Alleen het meest recente bericht kan bewerkt worden + Het bericht is te oud + + + Gallerij + Video + Bestand + Camera + Contact + Locatie + + + Locatie + Kaart + Satelliet + Hybride + + + Profiel + Gedeeld + Documenten + Media + Instellingen + Meldingen + Geluidsmeldingen + Blokkeer gebruiker + Blokkering opheffen + Blokkeer {user}? + + Verwijder uit contacten + Voeg toe aan contactpersonen + Oproep + Videogesprek + Nieuw bericht + + Mobiele telefoon + + Aan lijst met contactpersonen toe aan het voegen… + Verwijderen uit lijst met contactpersonen… + + Hernoem + Bel + Verwijder uit contactpersonen + Voeg toe aan contactpersonen + Beveiliging + Kan gebruiker niet gevonden krijgen + + + Groepsinformatie + Informatie over het kanaal + Leden + {0} van {1} + + beheerder + + Beheerd door {0} + Door mij beheerd + + Aan groep toevoegen… + Uit groep verwijderen… + + Bericht {0} + Bellen {0} + Bekijken {0} + Verwijder {0} uit de groep + + Bewerk + Wijzig thema + Wijzig profielfoto + Token voor andere diensten + + Gedeeld + Documenten + + Voeg een lid toe + Voeg een lid toe + Leden toevoegen + Groep verlaten + Kanaal verlaten + Beheer + Beheerder maken + Beheerdersrechten intrekken + + Beheer groep + Beheer kanaal + + Systeemrechten + Beheer wat mogelijk is in deze groep + Beheer wat mogelijk is in dit kanaal + + Soort groep + Soort kanaal + Openbaar + Openbaar + Besloten + Besloten + Verwijder groep + Alle berichten uit deze groep zullen verloren gaan. + Kanaal verwijderen + Alle berichten uit dit kanaal zullen verloren gaan. + + Deel gespreksgeschiedenis + Gedeeld + Alle leden zullen alle berichten uit de geschiedenis kunnen zien + + Alle leden kunnen groepsinformatie wijzigen + Alleen beheerders kunnen groepsinformatie wijzigen + Alle leden kunnen uitnodingen versturen + Laat bericht zien wanneer iemand de groep binnen komt en verlaat + Label van beheerder is voor iedereen zichtbaar + + Alle leden kunnen informatie over het kanaal wijzigen + Alleen beheerders kunnen informatie over het kanaal wijzigen + Alle leden kunnen uitnodigingen versturen + Laat bericht zien wanneer iemand het kanaal binnen komt en verlaat + Label van beheerder is voor iedereen zichtbaar + + Openbare groep + Openbare groepen kunnen gezocht en gevonden worden, iedereen kan lid worden. + Besloten groep + Bij besloten groepen kan alleen via een uitnodiging het lidmaatschap verkregen worden. + + Openbaar kanaal + Openbare groepen kunnen gezocht en gevonden worden, iedereen kan lid worden. + Besloten kanaal + Bij besloten kanalen kan alleen via een uitnodiging het lidmaatschap verkregen worden. + + + Geen lid van deze groep + Over deze groep + Groepsbeschrijving instellen + Groepsonderwerp + Groepsonderwerp instellen + + bot + + Korte groepsnaam kon niet gewijzigd worden + + + + Profielfoto + Foto + Groepsafbeelding + Geen afbeelding + Bewerken + Instellen als… + + + Contact toevoegen + Geef een e-mail, gebruikersnaam of telefoonnummer op + ANNULEER + VERDER + + + Nieuw bericht + + + Wijzig groep + Naam van de groep + Geef een groepsonderwerp op en stel eventueel een afbeelding in + Zoek op naam + Klaar + + + Maak kanaal + Naam van het kanaal + Geef een naam van het kanaal op en stel eventueel een afbeelding in + + Naam + Beschrijving + + + Lid toevoegen + + + Uitnodiging + Iemand die {appName} geinstalleerd heeft zal toegang tot de groep kunnen krijgen door de link te gebruiken + Kopieer link + Intrekken + Delen + Link gekopieerd naar klembord + Groepsuitnodiging via link + Niet in staat link te maken + Niet in staat link in te trekken + Uitnodiging link laden + + Lid worden van "%1$s"? + + + Documenten + Nog geen documenten + Bestand opgeslagen: + + + Geef een naam op + Geef een naam van de contactpersoon + Geef een groepsnaam op + Geef een groepsonderwerp op + Geef een gebruikersnaam op + ANNULEER + OPSLAAN + Wijzig naam… + Gebruikersnaam wijzigen… + Beschrijving wijzigen… + Groepsonderwerp wijzigen… + Vertel mensen iets kort. + + + + Help + Over + Over de app en onze diensten + FAQ + Veelgestelde vragen + Feedback + Geef ons terugkoppeling + Applicatie versie + App versie naar klembord gekopieerd + Stuur e-mail + Feedback + + + Meldingen + Gespreksgeluiden + Speel geluiden af bij in- en uitgaande berichten. + + Trillen + Trillen bij meldingen inschakelen + Groep + Inschakelen groepsmeldingen + Alleen wanneer ik direct aangesproken word door iemand + Groepsmeldingen alleen bij direct aanspreken inschakelen + Geef namen en berichten weer + Geef berichttekst en afzender weer in meldingenvenster + Geluid + Inschakelen geluiden bij meldingen + Geluid instellen + Geluidsmelding instellen + " berichten in " + " gesprekken" + " berichten" + + + Kies iemand + + + Gespreksinstellingen + Versturen bij [Enter] + Gebruik van de [Enter] toets zal een bericht verzenden in plaats van een nieuwe regel + + Automatisch animatie afspelen + Na downloaden van animaties deze gelijk automatisch afspelen + + Markdown + Of markdown inschakeld is + + Automatisch animaties ophalen + Automatisch afbeeldingen ophalen + Automatisch videos ophalen + Automatisch geluidsopnames ophalen + Automatisch documenten ophalen + + + Beveiliging en privacy + Alle sessies beëindigen + Op alle apparaten afmelden + Op alle andere apparaten afmelden? Alle tijdelijke applicatiegegevens op die apparaten zal verloren gaan. + Dit apparaat afmelden? Alle lokale applicatiegegevens zullen verloren gaan op {device} + Gevalideerde apparaten en diensten + Laden… + Niet in staat authorisatie in te trekken + (Dit) + Niet in staat te laden. Probeer het opnieuw. + + + Voor het laatst gezien + Wijzig wie kan zien wanneer ik voor het laatst online was. + Iedereen + Mijn contactpersonen + Niemand + + + Lijst met geblokkeerde contacten + Er wordt niemand geblokkeerd + Lijst met blokkeringen laden… + OPHEFFEN + + + + Definitief {0} berichten verwijderen? + Verwijder + + Definitief ALLE berichten uit dit gesprek wissen? + Verwijder + + Verwijder {0} contactpersonen? Zeker weten? + Verwijder + + Toevoegen van {0} aan de groep? + Toevoegen + + Verwijderen van {0} uit de groep? + Verwijder + + "{0}" staat nog niet geregistreerd in {appName}. Uitnodiging voor {appName} versturen? + Uitnodigen + + + Media + Afbeelding + %d van %d + geen recente emoji gevonden + + + Oproep beëindigd + Binnenkomende oproep + Gaat over… + Verbinden… + geluidsspeaker + microfoon uitzetten + video + berichten + gebruiker toevoegen + terug naar oproep + + + Groep "%1$s" verlaten? Zeker weten? + Kanaal "%1$s" verlaten? Zeker weten? + Verlaten + + Verwijder "%1$s" groep? + Verwijder "%1$s" kanaal? + Verwijder + + Verwijder gesprek met "%1$s"? + Verwijder + + Intrekken link? + Intrekken + + Intrekken token? + Intekken + + Deel met + + + Poging verbinding te maken mislukt. Verzeker dat er een werkende internetverbinding is en probeer het opnieuw.\n\nAls de problemen aanhouden, probeer dan of de telefoon opnieuw opstarten helpt. + Niet mogelijk de actie uit te voeren.\n\nControleer het toestel en herstart de telefoon indien problemen aanhouden. + Onbekend URL type + + diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-pt-rBR/ui_text.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-pt-rBR/ui_text.xml index 9e788e621f..6146214650 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-pt-rBR/ui_text.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-pt-rBR/ui_text.xml @@ -268,7 +268,7 @@ Foto Foto do grupo Sem foto - Editar + Editar Definir como… diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-pt-rPT/ui_text.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-pt-rPT/ui_text.xml index 9b3e91e6fb..bff00d1cec 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-pt-rPT/ui_text.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-pt-rPT/ui_text.xml @@ -297,7 +297,7 @@ Foto Foto do grupo Ainda não tem foto - Editar + Editar Definir como diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-ru/ui_text.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-ru/ui_text.xml index 6be1c7cf53..2b12068b11 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-ru/ui_text.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-ru/ui_text.xml @@ -19,6 +19,7 @@ Загрузка Загрузка… + Смена ника… Нет SD-карты Телефон скопирован в буфер обмена @@ -77,6 +78,13 @@ Использовать email для регистрации Использовать телефон для регистрации + Privacy Policy + Terms of Service + + Использовать альтернативный сервер + По умолчанию + + Выбор способа авторизации Пожалуйста, выберите способ авторизации. @@ -145,7 +153,7 @@ Начните набирать для поиска - Общий поиск + Глобальный поиск Ничего не найдено Помощь Поиск @@ -179,7 +187,7 @@ Оповещения Настройки чата Домашняя страница - Безопасность + Безопасность и Приватность Заблокированные пользователи Добавить ник Вы можете выбрать ник в {appName}. Так другие люди смогут найти вас без номера телефона.\n\nВы можете использовать латинские буквы, цифры и подчеркивание. Минимальная длина - 5 символов. @@ -190,6 +198,18 @@ О пользователе Обои + + добвить в разговор + Соединяем… + тихо + громкая связь + видео + Вызов окончен + Входящий вызов + Вернуться к вызову + Звоним… + сообщения + Позвонить {0} Отправить письмо {0} @@ -202,7 +222,7 @@ Нажмите и удерживайте для дополнительной информации Начать общение - Выберите контакт из списка контактов или нажмине кнопку "плюс" для начала общения. + Выберите контакт из списка контактов или нажмине кнопку "написать" для начала общения. Посмотреть контакт Переименовать контакт @@ -213,6 +233,12 @@ Выйти из группы Удалить группу + Информация о канале + Переименовать канал + Выйти из канала + Удалить канал + + Контакты Еще нет контактов @@ -226,7 +252,7 @@ Пригласите своих друзей Никто из ваших контактов не использует {appName}. Используйте кнопку ниже что бы пригласить их. РАССКАЗАТЬ ДРУЗЬЯМ - или добавьте контакт вручную, нажав на плюс ниже + или добавьте контакт вручную, используя поиск на главном экране Отправить ссылку @@ -274,6 +300,9 @@ Вы можете редактировать только последнее сообщение Вы можете редактировать только недавно отправленные сообщения + СТАРТ + Войти + Галерея Видео @@ -300,9 +329,10 @@ Заблокировать пользователя Разблокировать пользователя Заблокировать {user}? - В Контактах + Убрать из контактов Добавить в контакты Звонок + Видео звонок Новое сообщение Добавление в Контакты… @@ -317,14 +347,15 @@ Информация о группе + Информация о канале Участники {0} из {1} админ. - Создан {0} - Создан Вами + Владелец {0} + Вы владелец Добавление в группу… Исключение из группы… @@ -334,8 +365,6 @@ Инф. о {0} Исключить {0} из группы - Добавить пользователя - Покинуть группу Изменить название Изменить фото Токен интеграции @@ -344,6 +373,10 @@ Документы Добавить в группу + Добавить в канал + Добавить в канал + Покинуть группу + Покинуть канал Вы не участник данной группы @@ -351,12 +384,67 @@ Добавить описание группы Тема группы Добавить тему группы + + Администрирование группы + Администрирование + Назначить администратором + Аннулировать права администратора + Удалить группу + Вы потеряете все сообщения в этой группе + Описание + Редактировать + Имя + Разрешения + Управление правами пользователей в этой группе + В приватную группу можно попасть только по личному приглашению + Приватная группа + Публичные группы доступны для поиска, кто угодно можнет вступить в такую группу + Публичная группа + + В приватый канал можно попасть только по личному приглашению + Приватый канал + Публичные каналы доступны для поиска, кто угодно можнет вступить в такой канал + Публичный канал + + Поделиться историей + Все сообщения будут доступны всем участникам + Тип группы + Приватная + Приватный + Публичная + Публичный + + + Администрирование канала + w + Все пользователи могут редактировать информацию о группе + Администраторы могут редактировать информацию о группе + Все пользователи могут приглашать в группу + Показывать сообщения о входе/выходе из группы + Показывать информацию о том, кто является администратором пользователям + + Все пользователи могут редактировать иформацию о канале + Администраторы могут редактировать информацию о канале + Все пользователи могут приглашать в канал + Показывать сообщения о входе/выходе из канала + Показывать информацию о том, кто является администратором пользователям + + Удалить канал + Вы потеряете все сообщения в этом канале + Управление разрешениями в этом канале + Тип канала + + Введите имя канала и выбирете изображение + Название канала + Создать канал + Создать канал + Ваше фото Фото Фото группы Нет фото - Изменить + Изменить Установить как… @@ -462,6 +550,10 @@ Только для упоминаний Включить оповещения в группах только для сообщений с упоминанием + " сообщений в " + " чатах" + " сообщений" + Выберите человека @@ -477,14 +569,28 @@ Использование разметки markdown + Автоматически загружать анимации + Автоматически загружать изобажения + Автоматически загружать видео + Автоматически загружать аудио + Автоматически загружать документы + + - Безопасность + Безопасность и Приватность Уничтожить все сессии Произвести выход на всех устройствах Вы уверены, что хотите уничтожить сессии на всех устройствах? Все данные приложения на них будут удалены. Авторизованные устройства и сервисы Загрузка… + + Последняя активность + Укажите, кто может видеть вашу последнюю активность. + Все + Контакты + Никто + Заблокированные пользователи Вы никого не блокировали @@ -519,9 +625,11 @@ Вы уверены, что хотите выйти из группы "%1$s"? + Вы уверены, что хотите выйти из канала "%1$s"? Выйти Вы уверены, что хотите удалить группу "%1$s"? + Вы уверены, что хотите удалить канал "%1$s"? Удалить Вы уверены, что хотите удалить чат с "%1$s"? @@ -541,5 +649,13 @@ Невозможно произвести операцию.\n\nПожалуйста, проверьте свой телефон и выключите и включите его, если проблема не будет решена. Неизвестный тип ссылки + + Невозможно поменять ник + Невозможно очистить чат + Невозможно создать группу + Невозможно удалить чат + Невозможно покинуть группу + Поделиться с + diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-zh/ui_text.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-zh/ui_text.xml index 914703254a..e893710974 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-zh/ui_text.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values-zh/ui_text.xml @@ -269,7 +269,7 @@ 头像 群组头像 未设置头像 - 编辑 + 编辑 设置为… diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/colors.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/colors.xml index 51eba4a6c7..9a50e6d8c5 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/colors.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/colors.xml @@ -60,6 +60,10 @@ #44000000 #66000000 + #ffffff + #999999 + #dddddd + diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/ids.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/ids.xml index f094b04213..126145cee3 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/ids.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/ids.xml @@ -8,4 +8,5 @@ + \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/placeholder.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/placeholder.xml index bdcee824c7..e73bb820f4 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/placeholder.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/placeholder.xml @@ -4,11 +4,4 @@ #ffb8b8b8 - #509dbb - #52b3cd - #7e6eeb - #de5447 - #ee7e37 - #ed608b - #6ac53c \ No newline at end of file diff --git a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/ui_text.xml b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/ui_text.xml index d6463900dc..7ff38a5215 100644 --- a/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/ui_text.xml +++ b/actor-sdk/sdk-core-android/android-sdk/src/main/res/values/ui_text.xml @@ -1,5 +1,4 @@ - - @@ -107,6 +106,9 @@ By signing up, you agree to the Privacy Policy. Others users will be able to find you via email or phone number. + Change server endpoint + reset to default + Terms of Service Privacy Policy @@ -143,7 +145,6 @@ Please, enter your phone number Please, enter your email The code has expired. Please perform validation again. - au The code is invalid. Please try again. Authorization server error. Please, try again. @@ -159,6 +160,7 @@ Compose Create group + Create channel Join public group Add contact @@ -184,7 +186,7 @@ Home page Notifications Chat Settings - Security + Security and Privacy Blocked list Set username Change username @@ -206,7 +208,7 @@ Chats Long press for additional options Start messaging - Pick a contact from your contact list or press the plus button to start a conversation right now. + Pick a contact from your contact list or press the "write" button to start a conversation right now. View contact Rename contact @@ -217,6 +219,11 @@ Exit group Delete group + View channel info + Rename channel + Exit channel + Delete channel + Contacts No contacts here yet @@ -226,7 +233,7 @@ Invite your friends None of your contacts are using {appName}. Use the button below to invite them. Tell a friend - or manually add contacts by pressing the plus button below. + or manually add contacts using global search on main screen. Remove {0} from contacts Removing… @@ -268,6 +275,12 @@ SLIDE TO CANCEL You are not a member of this group + Group deleted + Channel deleted + + Join + Mute + Unmute START No description for this bot yet @@ -305,13 +318,15 @@ Media Settings Notifications + Notifications sound Block user Unblock user Block {user}? - In Contacts + Remove from contacts Add to Contacts Voice Call + Video Call New Message Mobile phone @@ -328,13 +343,14 @@ Group Info + Channel Info Members {0} of {1} admin - Created by {0} - Created by You + Owned by {0} + Owned by You Adding to group… Removing from group… @@ -344,8 +360,7 @@ View {0} Remove {0} from group - Add a member - Leave group + Edit Change theme Change photo Integration token @@ -354,9 +369,60 @@ Documents Add a member + Add a member + Add members + Leave group + Leave channel + Administration + Make admin + Revoke admin rights + + Group Administration + Channel Administration + + Permissions + Control what is possible in this group + Control what is possible in this channel + + Group Type + Channel Type + Public + Public + Private + Private + Delete Group + You will lose all messages in this group. + Delete Channel + You will lose all messages in this channel. + + Share History + Shared + All members will see all messages + + All members can edit group info + Admins can edit group info + All members can invite to group + Show member leave/join messages + Admin label is visible to all members + + All members can edit channel info + Admins can edit channel info + All members can invite to channel + Show member leave/join messages + Admin label is visible to all members + + Public Group + Public groups can be found in search, anyone can join them. + Private Group + Private group can only be joined via personal invitation. + + Public Channel + Public channels can be found in search, anyone can join them. + Private channel + Private channel can only be joined via personal invitation. - You are not a member of this group + You are not a member of this group About the group Set group description Group theme @@ -364,12 +430,15 @@ bot + Unable to change group short name + + Your photo Photo Group photo No photo - Edit + Edit Set as… @@ -388,6 +457,14 @@ Search by name Done + + Create channel + Name the channel + Enter channel name and set optional channel picture + + Name + Description + Add member @@ -403,6 +480,11 @@ Unable to revoke invite link Loading invite link + JOIN + OPEN + + Join "%1$s"? + Documents No documents yet @@ -440,8 +522,7 @@ Notifications Conversation tones Play sounds for incoming and outgoing messages. - Sound - Enable notification sounds + Vibration Enable vibration in notifications Group @@ -450,6 +531,10 @@ Enable group notifications only for mentions Show names and messages Show message text and sender name in notification panel + Sound + Enable notification sounds + Set sound + Set notification sound " messages in " " chats" " messages" @@ -468,8 +553,14 @@ Markdown Is markdown enabled + Auto download animations + Auto download images + Auto download videos + Auto download audio + Auto download documents + - Security + Security and Privacy Terminate all sessions Sign Out on all devices Are you sure you want to log out on all other devices? All app data will be lost on these devices. @@ -480,6 +571,13 @@ (This) Unable to load. Please try again. + + Last seen + Change who can see your Last Seen time. + Everybody + My Contacts + Nobody + Blocked list You haven\'t blocked anyone @@ -526,9 +624,11 @@ Are you sure you want to exit group "%1$s"? + Are you sure you want to exit channel "%1$s"? Exit Delete "%1$s" group? + Delete "%1$s" channel? Delete Delete chat with "%1$s"? diff --git a/actor-sdk/sdk-core-ios/ActorApp.xcodeproj/project.pbxproj b/actor-sdk/sdk-core-ios/ActorApp.xcodeproj/project.pbxproj index eea2566693..48b7ede315 100644 --- a/actor-sdk/sdk-core-ios/ActorApp.xcodeproj/project.pbxproj +++ b/actor-sdk/sdk-core-ios/ActorApp.xcodeproj/project.pbxproj @@ -207,6 +207,7 @@ EA6B7348F8364542DDD264F741AAAA19 = { CreatedOnToolsVersion = 6.4; DevelopmentTeam = HVJR44Y5B6; + LastSwiftMigration = 0800; SystemCapabilities = { com.apple.Maps.iOS = { enabled = 1; @@ -300,7 +301,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; 851EE1A34619AE6677649A27 /* Embed Pods Frameworks */ = { @@ -422,6 +423,7 @@ SDK_PATH = "../actor-sdk"; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; USER_HEADER_SEARCH_PATHS = ""; }; name = Debug; @@ -498,6 +500,7 @@ SDK_PATH = "../actor-sdk"; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; USER_HEADER_SEARCH_PATHS = ""; }; name = Release; diff --git a/actor-sdk/sdk-core-ios/ActorApp/AppDelegate.swift b/actor-sdk/sdk-core-ios/ActorApp/AppDelegate.swift index 3773b66dca..5f6c72d8ad 100644 --- a/actor-sdk/sdk-core-ios/ActorApp/AppDelegate.swift +++ b/actor-sdk/sdk-core-ios/ActorApp/AppDelegate.swift @@ -6,7 +6,7 @@ import Foundation import ActorSDK -@objc public class AppDelegate : ActorApplicationDelegate { +open class AppDelegate : ActorApplicationDelegate { override init() { super.init() @@ -14,7 +14,7 @@ import ActorSDK ActorSDK.sharedActor().inviteUrlHost = "quit.email" ActorSDK.sharedActor().inviteUrlScheme = "actor" - ActorSDK.sharedActor().style.searchStatusBarStyle = .Default + ActorSDK.sharedActor().style.searchStatusBarStyle = .default // Enabling experimental features ActorSDK.sharedActor().enableExperimentalFeatures = true @@ -26,28 +26,30 @@ import ActorSDK // Setting Development Push Id ActorSDK.sharedActor().apiPushId = 868547 - ActorSDK.sharedActor().authStrategy = .PhoneEmail + ActorSDK.sharedActor().authStrategy = .phoneEmail ActorSDK.sharedActor().style.dialogAvatarSize = 58 + ActorSDK.sharedActor().autoJoinGroups = ["actor_news"] + // Creating Actor ActorSDK.sharedActor().createActor() } - public override func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool { + open override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]?) -> Bool { super.application(application, didFinishLaunchingWithOptions: launchOptions) ActorSDK.sharedActor().presentMessengerInNewWindow() - return true; + return true } - public override func actorRootControllers() -> [UIViewController]? { + open override func actorRootControllers() -> [UIViewController]? { return [AAContactsViewController(), AARecentViewController(), AASettingsViewController()] } - public override func actorRootInitialControllerIndex() -> Int? { + open override func actorRootInitialControllerIndex() -> Int? { return 0 } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK-iOS.podspec b/actor-sdk/sdk-core-ios/ActorSDK-iOS.podspec index 36f1bd8a23..b0b461e8eb 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK-iOS.podspec +++ b/actor-sdk/sdk-core-ios/ActorSDK-iOS.podspec @@ -16,9 +16,9 @@ Pod::Spec.new do |s| # Core s.dependency 'RegexKitLite' - s.dependency 'CocoaAsyncSocket' s.dependency 'zipzap' s.dependency 'J2ObjC-Framework' + s.dependency 'ReachabilitySwift' # UI s.dependency 'VBFPopFlatButton' diff --git a/actor-sdk/sdk-core-ios/ActorSDK.xcodeproj/project.pbxproj b/actor-sdk/sdk-core-ios/ActorSDK.xcodeproj/project.pbxproj index 5cd8e6695a..75f6063376 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK.xcodeproj/project.pbxproj +++ b/actor-sdk/sdk-core-ios/ActorSDK.xcodeproj/project.pbxproj @@ -224,6 +224,7 @@ 066A53371BC537CA000E606E /* ConversationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 066A53361BC537CA000E606E /* ConversationViewController.swift */; }; 066A53391BC5456B000E606E /* ActorStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 066A53381BC5456B000E606E /* ActorStyle.swift */; }; 066CBCDC1C8D419F004507E2 /* AAAuthEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 066CBCDB1C8D419F004507E2 /* AAAuthEmailViewController.swift */; }; + 067B67541D45341D00B9A238 /* AAGroupViewMembersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067B67531D45341D00B9A238 /* AAGroupViewMembersController.swift */; }; 068AFC391C94A0050055F503 /* AADialogListProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068AFC381C94A0050055F503 /* AADialogListProcessor.swift */; }; 069CF4CC1BCB909A00C66E12 /* CLBackspaceDetectingTextField.h in Headers */ = {isa = PBXBuildFile; fileRef = 069CF4C41BCB909A00C66E12 /* CLBackspaceDetectingTextField.h */; }; 069CF4CD1BCB909A00C66E12 /* CLBackspaceDetectingTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 069CF4C51BCB909A00C66E12 /* CLBackspaceDetectingTextField.m */; }; @@ -233,11 +234,15 @@ 069CF4D11BCB909A00C66E12 /* CLTokenInputView.m in Sources */ = {isa = PBXBuildFile; fileRef = 069CF4C91BCB909A00C66E12 /* CLTokenInputView.m */; }; 069CF4D21BCB909A00C66E12 /* CLTokenView.h in Headers */ = {isa = PBXBuildFile; fileRef = 069CF4CA1BCB909A00C66E12 /* CLTokenView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 069CF4D31BCB909A00C66E12 /* CLTokenView.m in Sources */ = {isa = PBXBuildFile; fileRef = 069CF4CB1BCB909A00C66E12 /* CLTokenView.m */; }; + 06ABFE331D3FAF800031A0D6 /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 06ABFE2F1D3FAF800031A0D6 /* GCDAsyncSocket.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 06ABFE341D3FAF800031A0D6 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 06ABFE301D3FAF800031A0D6 /* GCDAsyncSocket.m */; }; + 06ABFE381D3FCCE30031A0D6 /* AAGroupEditInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06ABFE371D3FCCE30031A0D6 /* AAGroupEditInfoViewController.swift */; }; + 06ABFE3A1D410D2D0031A0D6 /* AAGroupTypeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06ABFE391D410D2D0031A0D6 /* AAGroupTypeController.swift */; }; + 06ABFE3D1D41283A0031A0D6 /* AAGroupAdministrationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06ABFE3C1D41283A0031A0D6 /* AAGroupAdministrationViewController.swift */; }; 06B489ED1C9F6EBD0054245B /* AAStickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B489EC1C9F6EBC0054245B /* AAStickerView.swift */; }; 06C1D0771C8BC9FC00B73632 /* AAAuthNameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C1D0761C8BC9FC00B73632 /* AAAuthNameViewController.swift */; }; 06C1D07B1C8BFE5C00B73632 /* AAAuthPhoneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C1D07A1C8BFE5C00B73632 /* AAAuthPhoneViewController.swift */; }; 06C1D07E1C8D0DEA00B73632 /* Telephony.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C1D07D1C8D0DE900B73632 /* Telephony.swift */; }; - 06CE898A1BD8401C005A5530 /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06CE89891BD8401C005A5530 /* Reachability.swift */; }; 06CE898C1BD841C9005A5530 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06CE898B1BD841C9005A5530 /* SystemConfiguration.framework */; }; 06CE89901BD84DF5005A5530 /* ActorSDKAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06CE898F1BD84DF5005A5530 /* ActorSDKAnalytics.swift */; }; 06D5C0571C8D6E20002D5045 /* AAAuthLogInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06D5C0561C8D6E20002D5045 /* AAAuthLogInViewController.swift */; }; @@ -596,6 +601,7 @@ 066A53361BC537CA000E606E /* ConversationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationViewController.swift; sourceTree = ""; }; 066A53381BC5456B000E606E /* ActorStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActorStyle.swift; sourceTree = ""; }; 066CBCDB1C8D419F004507E2 /* AAAuthEmailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AAAuthEmailViewController.swift; sourceTree = ""; }; + 067B67531D45341D00B9A238 /* AAGroupViewMembersController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AAGroupViewMembersController.swift; sourceTree = ""; }; 068AFC381C94A0050055F503 /* AADialogListProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AADialogListProcessor.swift; sourceTree = ""; }; 069CF4C41BCB909A00C66E12 /* CLBackspaceDetectingTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CLBackspaceDetectingTextField.h; sourceTree = ""; }; 069CF4C51BCB909A00C66E12 /* CLBackspaceDetectingTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CLBackspaceDetectingTextField.m; sourceTree = ""; }; @@ -605,11 +611,15 @@ 069CF4C91BCB909A00C66E12 /* CLTokenInputView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CLTokenInputView.m; sourceTree = ""; }; 069CF4CA1BCB909A00C66E12 /* CLTokenView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CLTokenView.h; sourceTree = ""; }; 069CF4CB1BCB909A00C66E12 /* CLTokenView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CLTokenView.m; sourceTree = ""; }; + 06ABFE2F1D3FAF800031A0D6 /* GCDAsyncSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncSocket.h; sourceTree = ""; }; + 06ABFE301D3FAF800031A0D6 /* GCDAsyncSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDAsyncSocket.m; sourceTree = ""; }; + 06ABFE371D3FCCE30031A0D6 /* AAGroupEditInfoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AAGroupEditInfoViewController.swift; sourceTree = ""; }; + 06ABFE391D410D2D0031A0D6 /* AAGroupTypeController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AAGroupTypeController.swift; sourceTree = ""; }; + 06ABFE3C1D41283A0031A0D6 /* AAGroupAdministrationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AAGroupAdministrationViewController.swift; sourceTree = ""; }; 06B489EC1C9F6EBC0054245B /* AAStickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AAStickerView.swift; sourceTree = ""; }; 06C1D0761C8BC9FC00B73632 /* AAAuthNameViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AAAuthNameViewController.swift; sourceTree = ""; }; 06C1D07A1C8BFE5C00B73632 /* AAAuthPhoneViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AAAuthPhoneViewController.swift; sourceTree = ""; }; 06C1D07D1C8D0DE900B73632 /* Telephony.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Telephony.swift; sourceTree = ""; }; - 06CE89891BD8401C005A5530 /* Reachability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reachability.swift; sourceTree = ""; }; 06CE898B1BD841C9005A5530 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; 06CE898F1BD84DF5005A5530 /* ActorSDKAnalytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActorSDKAnalytics.swift; sourceTree = ""; }; 06D5C0561C8D6E20002D5045 /* AAAuthLogInViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AAAuthLogInViewController.swift; sourceTree = ""; }; @@ -974,7 +984,6 @@ 066A51581BC4C14A000E606E /* AASwiftlyLRU.swift */, 066A51641BC4C366000E606E /* AATools.swift */, 065975381BC7CA7B00B8C7DF /* Bundle.swift */, - 06CE89891BD8401C005A5530 /* Reachability.swift */, 06C1D07D1C8D0DE900B73632 /* Telephony.swift */, ); path = Utils; @@ -1092,7 +1101,6 @@ 066A525F1BC50E53000E606E /* Controllers */ = { isa = PBXGroup; children = ( - 9AAE96461CF81F140092E366 /* Ringtones */, 06C1D0751C8BC55100B73632 /* Welcome */, 066A52601BC50E6B000E606E /* Auth */, 066A52F91BC52FA0000E606E /* Compose */, @@ -1102,6 +1110,7 @@ 06E7B2451C0F8D410090660C /* Location */, 066A53051BC5317B000E606E /* Recent */, 066A527B1BC51EC6000E606E /* Root */, + 9AAE96461CF81F140092E366 /* Ringtones */, 066A52EB1BC52AF8000E606E /* Settings */, 066A53091BC53197000E606E /* User */, 06E323131C6A7AA300D66F53 /* Calls */, @@ -1228,8 +1237,12 @@ isa = PBXGroup; children = ( 066A52E81BC52A25000E606E /* Cells */, - 066A52E31BC52A20000E606E /* AAAddParticipantViewController.swift */, 066A52E21BC52A20000E606E /* AAGroupViewController.swift */, + 06ABFE371D3FCCE30031A0D6 /* AAGroupEditInfoViewController.swift */, + 067B67531D45341D00B9A238 /* AAGroupViewMembersController.swift */, + 06ABFE3C1D41283A0031A0D6 /* AAGroupAdministrationViewController.swift */, + 06ABFE391D410D2D0031A0D6 /* AAGroupTypeController.swift */, + 066A52E31BC52A20000E606E /* AAAddParticipantViewController.swift */, 066A52E41BC52A20000E606E /* AAInviteLinkViewController.swift */, ); path = Group; @@ -1416,6 +1429,15 @@ path = CLTokenInputView; sourceTree = ""; }; + 06ABFE261D3FAF1A0031A0D6 /* CocoaAsyncSocket */ = { + isa = PBXGroup; + children = ( + 06ABFE2F1D3FAF800031A0D6 /* GCDAsyncSocket.h */, + 06ABFE301D3FAF800031A0D6 /* GCDAsyncSocket.m */, + ); + name = CocoaAsyncSocket; + sourceTree = ""; + }; 06C1D0751C8BC55100B73632 /* Welcome */ = { isa = PBXGroup; children = ( @@ -1436,6 +1458,7 @@ 06E322CB1C69392F00D66F53 /* Libs */ = { isa = PBXGroup; children = ( + 06ABFE261D3FAF1A0031A0D6 /* CocoaAsyncSocket */, 06E164921C96FF15005AFB94 /* CommonCrypto */, 061850A31C95CBF000C522D5 /* YYKit */, 15D35F0A1C20182900E3717A /* AudioRecorder */, @@ -1813,6 +1836,7 @@ 152AA8AA1C2989270030DEEE /* SLKTextView.h in Headers */, 152AA8B01C2989270030DEEE /* SLKTypingIndicatorProtocol.h in Headers */, 152AA8AC1C2989270030DEEE /* SLKTextView+SLKAdditions.h in Headers */, + 06ABFE331D3FAF800031A0D6 /* GCDAsyncSocket.h in Headers */, 152AA8AE1C2989270030DEEE /* SLKTextViewController.h in Headers */, 152AA8B31C2989270030DEEE /* SLKUIConstants.h in Headers */, 152AA8B61C2989270030DEEE /* UIScrollView+SLKAdditions.h in Headers */, @@ -1909,6 +1933,7 @@ 066A50D11BC4AE63000E606E = { CreatedOnToolsVersion = 7.0; DevelopmentTeam = HVJR44Y5B6; + LastSwiftMigration = 0800; }; }; }; @@ -1993,7 +2018,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; showEnvVarsInLog = 0; }; A2ED258362D73946D3AE7FB4 /* [CP] Copy Pods Resources */ = { @@ -2039,6 +2064,7 @@ 061850DB1C95CBF000C522D5 /* YYTextContainerView.m in Sources */, 15D35F301C20187200E3717A /* info.c in Sources */, 066A532E1BC53406000E606E /* AABubbleBaseFileCell.swift in Sources */, + 06ABFE3D1D41283A0031A0D6 /* AAGroupAdministrationViewController.swift in Sources */, 066A514A1BC4BCE0000E606E /* Dispatch.swift in Sources */, 066A52401BC4EECD000E606E /* AABigPlaceholderView.swift in Sources */, BED5A1F51C48396A0045FDB0 /* NYTPhotosDataSource.m in Sources */, @@ -2073,6 +2099,7 @@ 061850E31C95CBF000C522D5 /* YYTextKeyboardManager.m in Sources */, 066A52571BC4EF61000E606E /* AACollectionViewController.swift in Sources */, 066A532F1BC53406000E606E /* AABubbleMediaCell.swift in Sources */, + 06ABFE3A1D410D2D0031A0D6 /* AAGroupTypeController.swift in Sources */, 15D35F631C20187E00E3717A /* AAModernConversationAudioPlayer.m in Sources */, 066A527F1BC51ED0000E606E /* AARootSplitViewController.swift in Sources */, 066A52481BC4EED5000E606E /* AAProgressView.swift in Sources */, @@ -2114,6 +2141,7 @@ 066A51241BC4B56D000E606E /* Colors.swift in Sources */, 066A53171BC533DD000E606E /* AABubbles.swift in Sources */, 153F6B8B1C2D7BA400C0B960 /* AATapLabel.swift in Sources */, + 06ABFE341D3FAF800031A0D6 /* GCDAsyncSocket.m in Sources */, 061850E71C95CBF000C522D5 /* YYTextLine.m in Sources */, 066A52081BC4E962000E606E /* Makefile in Sources */, 066A53231BC533F5000E606E /* Caches.swift in Sources */, @@ -2232,7 +2260,6 @@ 061850ED1C95CBF000C522D5 /* YYTextArchiver.m in Sources */, 066A50E21BC4AF9F000E606E /* ActorSDK.swift in Sources */, 15D35F761C201B6B00E3717A /* AACustomPresentationAnimationController.swift in Sources */, - 06CE898A1BD8401C005A5530 /* Reachability.swift in Sources */, 06E7B24C1C0FAB500090660C /* AAMapFastView.swift in Sources */, BED5A1F11C48396A0045FDB0 /* NYTPhotoCaptionView.m in Sources */, BED5A20F1C4839880045FDB0 /* NSBundle+NYTPhotoViewer.m in Sources */, @@ -2260,7 +2287,9 @@ 066A52231BC4EEAC000E606E /* AAManagedSection.swift in Sources */, 066A52D11BC52204000E606E /* AADialogCell.swift in Sources */, 066A51901BC4C383000E606E /* CocoaNetworkRuntime.swift in Sources */, + 06ABFE381D3FCCE30031A0D6 /* AAGroupEditInfoViewController.swift in Sources */, 06E7B24A1C0F92140090660C /* AABubbleLocationCell.swift in Sources */, + 067B67541D45341D00B9A238 /* AAGroupViewMembersController.swift in Sources */, 066A52581BC4EF61000E606E /* Alerts.swift in Sources */, 066A51691BC4C366000E606E /* AATools.swift in Sources */, 066A53201BC533F5000E606E /* AABubbleBackgroundProcessor.swift in Sources */, @@ -2430,6 +2459,7 @@ SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/ActorSDK/Sources/Libs/CommonCrypto $(PROJECT_DIR)/IDZSwiftCommonCrypto/Frameworks/$(PLATFORM_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; VALID_ARCHS = "arm64 armv7"; WARNING_CFLAGS = "-Wno-nullability-completeness"; }; @@ -2485,6 +2515,7 @@ SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/ActorSDK/Sources/Libs/CommonCrypto $(PROJECT_DIR)/IDZSwiftCommonCrypto/Frameworks/$(PLATFORM_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; VALID_ARCHS = "arm64 armv7"; WARNING_CFLAGS = "-Wno-nullability-completeness"; }; diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Base.lproj/Localizable.strings b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Base.lproj/Localizable.strings index d689cea672..e54b96b6de 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Base.lproj/Localizable.strings +++ b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Base.lproj/Localizable.strings @@ -288,7 +288,78 @@ "GroupIntegrations" = "Integrations"; -"GroupMembers" = "{0} MEMBERS"; +"GroupViewMembers" = "Members"; + +"GroupDescription" = "Description"; + + + +"GroupAdministration" = "Administration"; + +"GroupTypeTitle" = "Group Type"; + +"GroupTypeTitleChannel" = "Channel Type"; + +"GroupPermissionsHint" = "Control what is possible in this group"; + +"GroupPermissionsHintChannel" = "Control what is possible in this channel"; + +"GroupTypeHintPublic" = "Public groups can be found in search and anyone can join"; + +"GroupTypeHintPrivate" = "Private groups can be joined only via personal invitation"; + +"GroupTypeHintPublicChannel" = "Public channels can be found in search and anyone can join"; + +"GroupTypeHintPrivateChannel" = "Private channels can be joined only via personal invitation"; + +"GroupTypeLinkHint" = "People can share this link with others and find your group using search"; + +"GroupTypeLinkHintChannel" = "People can share this link with others and find your channel using search"; + +"GroupTypePublic" = "Public"; + +"GroupTypePrivate" = "Private"; + +"ChannelTypePublic" = "Public"; + +"ChannelTypePrivate" = "Private"; + +"GroupTypePublicFull" = "Public Group"; + +"GroupTypePrivateFull" = "Private Group"; + +"ChannelTypePublicFull" = "Public Channel"; + +"ChannelTypePrivateFull" = "Private Channel"; + +"GroupDeleteHint" = "You will lose all messages in this group"; + +"GroupDeleteHintChannel" = "You will lose all messages in this channel"; + + + +"GroupShareTitle" = "Shared History"; + +"GroupShareEnabled" = "Shared"; + +"GroupShareHint" = "All members will see all messages"; + +"GroupShareMessage" = "Are you sure want to share all messages to all members? This action is irreversible."; + +"GroupShareAction" = "Share"; + + + +"GroupEditTitle" = "Edit Group"; + +"GroupEditTitleChannel" = "Edit Channel"; + +"GroupEditName" = "Group Name"; + +"GroupEditNameChannel" = "Channel Name"; + +"GroupEditDescription" = "Description"; + "GroupMemberAdmin" = "admin"; @@ -318,11 +389,6 @@ "GroupAddParticipantUrl" = "Invite to Group via Link"; -"GroupLeave" = "Leave group"; - -"GroupLeaveConfirm" = "Are you sure you want to leave group?"; - -"GroupLeaveConfirmAction" = "Leave"; "GroupInviteLinkPageTitle" = "Invite Link"; @@ -334,37 +400,36 @@ "GroupInviteLinkRevokeAction" = "Revoke"; -"GroupIntegrationPageTitle" = "Integration API"; - -"GroupIntegrationLinkTitle" = "LINK"; - -"GroupIntegrationLinkHint" = "You can use this link to send a message from any external system."; - -"GroupIntegrationDoc" = "Open Documentation"; - -"GroupIntegrationLinkRevokeMessage" = "Are you sure you want to revoke this link? Once you do, no one will be able to send messages to the group using it."; -"GroupIntegrationLinkRevokeAction" = "Revoke"; "GroupJoinMessage" = "Are you sure you want to join this group?"; "GroupJoinAction" = "Join"; -"GroupMembers" = "MEMBERS"; - /* * Compose */ "ComposeTitle" = "New Message"; + "CreateGroup" = "Create Group"; +"CreateChannel" = "Create Channel"; + + "CreateGroupTitle" = "Create Group"; -"CreateGroupHint" = "Please provide the group's subject and an optional group icon"; +"CreateGroupHint" = "Please provide the group name and an optional group icon"; + +"CreateGroupNamePlaceholder" = "Group Name"; + +"CreateChannelTitle" = "Create Channel"; + +"CreateChannelHint" = "Please provide the channel name and an optional channel icon"; + +"CreateChannelNamePlaceholder" = "Channel Name"; -"CreateGroupNamePlaceholder" = "Group Subject"; "CreateGroupMembersTitle" = "Invite members"; @@ -398,6 +463,10 @@ "Chat.MicrophoneAccessDisabled" = "We need access to your microphone for voice messages. Please go to Settings — Privacy — Microphone turn them ОN"; +"ChatDeleted" = "This chat was deleted by owner."; + +"ChatJoin" = "Join"; + /* * Chat attachment menu */ @@ -621,6 +690,47 @@ "ActionOpenCode" = "View Code"; +"ActionMute" = "Mute"; + +"ActionUnmute" = "Unmute"; + + +"ActionDelete" = "Delete"; + +"ActionDeleteMessage" = "Are you sure want to delete chat?"; + + +"ActionDeleteChannel" = "Delete Channel"; + +"ActionDeleteChannelMessage" = "Wait! Deleting this channel will remove all members and all messages will be lost. Delete the channel anyway?"; + +"ActionDeleteGroup" = "Delete Group"; + +"ActionDeleteGroupMessage" = "Wait! Deleting this group will remove all members and all messages will be lost. Delete the group anyway?"; + + +"ActionLeaveChannel" = "Leave Channel"; + +"ActionLeaveChannelMessage" = "Are you sure want to leave channel?"; + +"ActionLeaveChannelAction" = "Leave"; + + + +"ActionDeleteAndExit" = "Delete and Exit"; + +"ActionDeleteAndExitMessage" = "Are you sure want to exit group and delete all messages?"; + +"ActionDeleteAndExitAction" = "Exit"; + + + +"ActionClearHistory" = "Clear History"; + +"ActionClearHistoryMessage" = "Are you sure want to clear history?"; + +"ActionClearHistoryAction" = "Clear"; + /* * Network */ diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_create_channel.imageset/Contents.json b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_create_channel.imageset/Contents.json new file mode 100644 index 0000000000..63a34293ca --- /dev/null +++ b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_create_channel.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Megaphone Filled-32.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Megaphone Filled-64.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Megaphone Filled-96.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_create_channel.imageset/Megaphone Filled-32.png b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_create_channel.imageset/Megaphone Filled-32.png new file mode 100644 index 0000000000..d63f1a9413 Binary files /dev/null and b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_create_channel.imageset/Megaphone Filled-32.png differ diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_create_channel.imageset/Megaphone Filled-64.png b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_create_channel.imageset/Megaphone Filled-64.png new file mode 100644 index 0000000000..477f8a90eb Binary files /dev/null and b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_create_channel.imageset/Megaphone Filled-64.png differ diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_create_channel.imageset/Megaphone Filled-96.png b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_create_channel.imageset/Megaphone Filled-96.png new file mode 100644 index 0000000000..4f1516e690 Binary files /dev/null and b/actor-sdk/sdk-core-ios/ActorSDK/Resources/Images.xcassets/ic_create_channel.imageset/Megaphone Filled-96.png differ diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/es.lproj/Localizable.strings b/actor-sdk/sdk-core-ios/ActorSDK/Resources/es.lproj/Localizable.strings index 976879373a..c405b02364 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Resources/es.lproj/Localizable.strings +++ b/actor-sdk/sdk-core-ios/ActorSDK/Resources/es.lproj/Localizable.strings @@ -285,7 +285,78 @@ "GroupIntegrations" = "Integraciones"; -"GroupMembers" = "{0} MIEMBROS"; +"GroupViewMembers" = "Miembros"; + +"GroupDescription" = "Descripción"; + + + +"GroupAdministration" = "Administrador"; + +"GroupTypeTitle" = "Tipo de Grupo"; + +"GroupTypeTitleChannel" = "Tipo de Canal"; + +"GroupTypePublic" = "Público"; + +"GroupTypePrivate" = "Privado"; + +"ChannelTypePublic" = "Público"; + +"ChannelTypePrivate" = "Privado"; + +"GroupTypePublicFull" = "Grupo público"; + +"GroupTypePrivateFull" = "Grupo privado"; + +"ChannelTypePublicFull" = "Canal público"; + +"ChannelTypePrivateFull" = "Canal privado"; + +"GroupPermissionsHint" = "Controlar lo que es posible en este grupo"; + +"GroupPermissionsHintChannel" = "Controlar lo que es posible en este canal"; + +"GroupTypeHintPublic" = "Los grupos públicos se pueden encontrar en la búsqueda y cualquiera puede unirse"; + +"GroupTypeHintPrivate" = "Los grupos privados pueden unirse sólo a través de invitación personal"; + +"GroupTypeHintPublicChannel" = "Los canales públicos se pueden encontrar en la búsqueda y cualquiera puede unirse"; + +"GroupTypeHintPrivateChannel" = "Los canales privados se pueden unir únicamente a través de invitación personal"; + +"GroupTypeLinkHint" = "People can share this link with others and find your group using search"; + +"GroupTypeLinkHintChannel" = "Las personas pueden compartir este enlace con los demás y encontrar su grupo mediante la búsqueda"; + +"GroupDeleteHint" = "Perderá todos los mensajes de este grupo"; + +"GroupDeleteHintChannel" = "Usted perderá todos los mensajes de este canal"; + + + +"GroupShareTitle" = "Historial compartido"; + +"GroupShareEnabled" = "Compartir"; + +"GroupShareHint" = "Todos los miembros podrán ver todos los mensajes"; + +"GroupShareMessage" = "¿Seguro desea compartir todos los mensajes a todos los miembros? Esta acción es irreversible."; + +"GroupShareAction" = "Compartir"; + + +"GroupEditTitle" = "Editar Grupo"; + +"GroupEditTitleChannel" = "Editar Canal"; + +"GroupEditName" = "Nombre de Grupo"; + +"GroupEditNameChannel" = "Nombre del Canal"; + +"GroupEditDescription" = "Descripción"; + + "GroupMemberAdmin" = "admin"; @@ -315,11 +386,7 @@ "GroupAddParticipantUrl" = "Invitar al grupo a través de Enlace"; -"GroupLeave" = "Salir de grupo"; - -"GroupLeaveConfirm" = "¿Seguro que quieres dejar de grupo?"; -"GroupLeaveConfirmAction" = "Salir"; "GroupInviteLinkPageTitle" = "invitar por Enlace"; @@ -331,17 +398,7 @@ "GroupInviteLinkRevokeAction" = "Revocar"; -"GroupIntegrationPageTitle" = "Integración de API"; - -"GroupIntegrationLinkTitle" = "LINK"; - -"GroupIntegrationLinkHint" = "Puedes usar este enlace para enviar mensajes desde cualquier sistema externo."; - -"GroupIntegrationDoc" = "Abrir Documentación"; -"GroupIntegrationLinkRevokeMessage" = "¿Seguro que quieres revocar este enlace? Una vez hecho, nadie va a ser capaz de enviar mensajes al grupo de usarlo."; - -"GroupIntegrationLinkRevokeAction" = "Revocar"; "GroupJoinMessage" = "¿Seguro quieres unirte al grupo?"; @@ -353,13 +410,24 @@ "ComposeTitle" = "Nuevo mensaje"; + "CreateGroup" = "Crear grupo"; +"CreateChannel" = "Crear canal"; + + "CreateGroupTitle" = "Crear grupo"; "CreateGroupHint" = "Por favor, introduce un nombre de grupo e inserta una imagen."; -"CreateGroupNamePlaceholder" = "Grupo Asunto"; +"CreateGroupNamePlaceholder" = "Asunto del Grupo"; + +"CreateChannelTitle" = "Crear Canal"; + +"CreateChannelHint" = "Por favor, proporcione el nombre del canal y un icono de canal opcional"; + +"CreateChannelNamePlaceholder" = "Nombre del canal"; + "CreateGroupMembersTitle" = "Invitar a los miembros"; @@ -392,6 +460,9 @@ "ChatSlideUpToCancel" = "Deslizar para cancelar"; +"ChatDeleted" = "This chat was deleted by owner."; + +"ChatJoin" = "Join"; /* * Chat attachment menu @@ -419,7 +490,7 @@ "Placeholder_Empty_Title" = "Invita a tus amigos"; -"Placeholder_Empty_Message" = "Ninguno de tus contactos usar {appname}. Utiliza el botón de abajo para invitarlos."; +"Placeholder_Empty_Message" = "Ninguno de tus contactos usan {appname}. Utiliza el botón de abajo para invitarlos."; "Placeholder_Empty_Action" = "CONTAR A UN AMIGO"; @@ -609,7 +680,48 @@ "ActionAddPhoto2" = "foto"; -"ActionOpenCode" = "View Code"; +"ActionOpenCode" = "Ver código"; + +"ActionMute" = "Silenciar"; + +"ActionUnmute" = "No silenciado"; + + +"ActionDelete" = "Eliminar"; + +"ActionDeleteMessage" = "¿Seguro desea borrar el chat?"; + + +"ActionDeleteChannel" = "Eliminar canal"; + +"ActionDeleteChannelMessage" = "Wait! Deleting this channel will remove all members and all messages will be lost. Delete the channel anyway?"; + +"ActionDeleteGroup" = "Delete Group"; + +"ActionDeleteGroupMessage" = "¡Espere! La eliminación de este canal, eliminará todos los miembros y se perderán todos los mensajes. Eliminar el canal de todos modos?"; + + +"ActionLeaveChannel" = "Abandonar Canal"; + +"ActionLeaveChannelMessage" = "¿Seguro que quieres abandonar el canal?"; + +"ActionLeaveChannelAction" = "Abandonar"; + + + +"ActionDeleteAndExit" = "Eliminar y Salir"; + +"ActionDeleteAndExitMessage" = "¿Seguro desea salir del grupo y eliminar todos los mensajes?"; + +"ActionDeleteAndExitAction" = "Salir"; + + + +"ActionClearHistory" = "Borrar historial"; + +"ActionClearHistoryMessage" = "¿Seguro desea borrar el historial?"; + +"ActionClearHistoryAction" = "Borrar"; /* * Network @@ -629,4 +741,4 @@ "ErrorAlreadyJoined" = "Ya eres miembro del grupo"; -"ErrorUnableToJoin" = "Unable to join to group"; +"ErrorUnableToJoin" = "No es posible unirse al grupo"; \ No newline at end of file diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/pt.lproj/Localizable.strings b/actor-sdk/sdk-core-ios/ActorSDK/Resources/pt.lproj/Localizable.strings index e35ff1cafb..e867ef01ff 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Resources/pt.lproj/Localizable.strings +++ b/actor-sdk/sdk-core-ios/ActorSDK/Resources/pt.lproj/Localizable.strings @@ -276,7 +276,77 @@ "GroupIntegrations" = "Integrações"; -"GroupMembers" = "{0} MEMBROS"; +"GroupDescription" = "Description"; + + + +"GroupAdministration" = "Administration"; + +"GroupTypeTitle" = "Group Type"; + +"GroupTypeTitleChannel" = "Channel Type"; + +"GroupTypePublic" = "Public"; + +"GroupTypePrivate" = "Private"; + +"ChannelTypePublic" = "Public"; + +"ChannelTypePrivate" = "Private"; + +"GroupTypePublicFull" = "Public Group"; + +"GroupTypePrivateFull" = "Private Group"; + +"ChannelTypePublicFull" = "Public Channel"; + +"ChannelTypePrivateFull" = "Private Channel"; + +"GroupPermissionsHint" = "Control what is possible in this group"; + +"GroupPermissionsHintChannel" = "Control what is possible in this channel"; + +"GroupTypeHintPublic" = "Public groups can be found in search and anyone can join"; + +"GroupTypeHintPrivate" = "Private groups can be joined only via personal invitation"; + +"GroupTypeHintPublicChannel" = "Public channels can be found in search and anyone can join"; + +"GroupTypeHintPrivateChannel" = "Private channels can be joined only via personal invitation"; + +"GroupTypeLinkHint" = "People can share this link with others and find your group using search"; + +"GroupTypeLinkHintChannel" = "People can share this link with others and find your channel using search"; + +"GroupDeleteHint" = "You will lose all messages in this group"; + +"GroupDeleteHintChannel" = "You will lose all messages in this channel"; + + +"GroupShareTitle" = "Shared History"; + +"GroupShareEnabled" = "Shared"; + +"GroupShareHint" = "All members will see all messages"; + +"GroupShareMessage" = "Are you sure want to share all messages to all members? This action is irreversible."; + +"GroupShareAction" = "Share"; + + +"GroupEditTitle" = "Edit Group"; + +"GroupEditTitleChannel" = "Edit Channel"; + +"GroupEditName" = "Group Name"; + +"GroupEditNameChannel" = "Channel Name"; + +"GroupEditDescription" = "Description"; + + + +"GroupViewMembers" = "Membros"; "GroupMemberAdmin" = "admin"; @@ -306,11 +376,7 @@ "GroupAddParticipantUrl" = "COnvidar para o grupo via Link"; -"GroupLeave" = "Sair do grupo"; - -"GroupLeaveConfirm" = "Você tem certeza que quer sair do grupo?"; -"GroupLeaveConfirmAction" = "Sair"; "GroupInviteLinkPageTitle" = "Link de Convite"; @@ -322,38 +388,38 @@ "GroupInviteLinkRevokeAction" = "Remover"; -"GroupIntegrationPageTitle" = "Integração API"; - -"GroupIntegrationLinkTitle" = "LINK"; - -"GroupIntegrationLinkHint" = "Você pode usar este link para enviar mesnagens para qualquer sistema exteno."; - -"GroupIntegrationDoc" = "Abrir Documentação"; - -"GroupIntegrationLinkRevokeMessage" = "Você tem certeza que quer remover este link? Uma vez removido, mais ninguém sera capaz de enviar mensagens para o grupo usando este link."; - -"GroupIntegrationLinkRevokeAction" = "Remover"; - -"GroupMembers" = "MEMBERS"; - /* * Compose */ "ComposeTitle" = "Nova Mensagem"; + "CreateGroup" = "Criar Grupo"; +"CreateChannel" = "Create Channel"; + + "CreateGroupTitle" = "Criar Grupo"; "CreateGroupNamePlaceholder" = "Inserir nome do grupo"; "CreateGroupHint" = "Please provide group subject and optional group icon"; + +"CreateChannelTitle" = "Create Channel"; + +"CreateChannelHint" = "Please provide the channel name and an optional channel icon"; + +"CreateChannelNamePlaceholder" = "Channel Name"; + + "CreateGroupMembersTitle" = "Invite members"; "CreateGroupMembersPlaceholders" = "Enter Names"; +"ChatDeleted" = "This chat was deleted by owner."; + /* * Location */ @@ -380,6 +446,10 @@ "ChatNoGroupAccess" = "Você não é membro"; +"ChatDeleted" = "This chat was deleted by owner."; + +"ChatJoin" = "Join"; + /* * Find */ @@ -594,6 +664,47 @@ "ActionOpenCode" = "View Code"; +"ActionMute" = "Mute"; + +"ActionUnmute" = "Unmute"; + + +"ActionDelete" = "Delete"; + +"ActionDeleteMessage" = "Are you sure want to delete chat?"; + + +"ActionDeleteChannel" = "Delete Channel"; + +"ActionDeleteChannelMessage" = "Wait! Deleting this channel will remove all members and all messages will be lost. Delete the channel anyway?"; + +"ActionDeleteGroup" = "Delete Group"; + +"ActionDeleteGroupMessage" = "Wait! Deleting this group will remove all members and all messages will be lost. Delete the group anyway?"; + + +"ActionLeaveChannel" = "Leave Channel"; + +"ActionLeaveChannelMessage" = "Are you sure want to leave channel?"; + +"ActionLeaveChannelAction" = "Leave"; + + + +"ActionDeleteAndExit" = "Delete and Exit"; + +"ActionDeleteAndExitMessage" = "Are you sure want to exit group and delete all messages?"; + +"ActionDeleteAndExitAction" = "Exit"; + + + +"ActionClearHistory" = "Clear History"; + +"ActionClearHistoryMessage" = "Are you sure want to clear history?"; + +"ActionClearHistoryAction" = "Clear"; + /* * Network */ diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/ru.lproj/Localizable.strings b/actor-sdk/sdk-core-ios/ActorSDK/Resources/ru.lproj/Localizable.strings index 894dc49ff9..176a1b2015 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Resources/ru.lproj/Localizable.strings +++ b/actor-sdk/sdk-core-ios/ActorSDK/Resources/ru.lproj/Localizable.strings @@ -280,6 +280,7 @@ "GroupEditConfirm" = "Вы уверены, что хотите изменить тему группы?"; + "GroupEditConfirmAction" = "Изменить тему"; "GroupNotifications" = "Оповещения"; @@ -288,7 +289,80 @@ "GroupIntegrations" = "Интеграции"; -"GroupMembers" = "{0} УЧАСТНИКИ"; +"GroupDescription" = "Описание"; + + + +"GroupAdministration" = "Администрирование"; + +"GroupTypeTitle" = "Тип группы"; + +"GroupTypeTitleChannel" = "Тип канала"; + +"GroupTypePublic" = "Публичная"; + +"GroupTypePrivate" = "Приватная"; + +"ChannelTypePublic" = "Публичный"; + +"ChannelTypePrivate" = "Приватный"; + +"GroupTypePublicFull" = "Публичная группа"; + +"GroupTypePrivateFull" = "Приватная группа"; + +"ChannelTypePublicFull" = "Публичный канал"; + +"ChannelTypePrivateFull" = "Приватный канал"; + +"GroupPermissionsHint" = "Управление тем что возможно делать в этой группе"; + +"GroupPermissionsHintChannel" = "Управление тем что возможно делать в этом канале"; + +"GroupTypeHintPublic" = "Публичные гурппы могут быть найдены в поиске и кто угодно может войти в нее"; + +"GroupTypeHintPrivate" = "Вступить в приватную группу можно только по личному приглашению"; + +"GroupTypeHintPublicChannel" = "Публичные каналы могут быть найдены в поиске и кто угодно может подписаться на него"; + +"GroupTypeHintPrivateChannel" = "Вступить в приватный канал можно только по личному приглашению"; + +"GroupTypeLinkHint" = "Ваши друзья могут делиться этой ссылкой или находить группу в поиске"; + +"GroupTypeLinkHintChannel" = "Ваши друзья могут делиться этой ссылкой или находить канал в поиске"; + +"GroupDeleteHint" = "Вы потеряете всех сообщения в этой группе"; + +"GroupDeleteHintChannel" = "Вы потеряете все сообщения в этом канале"; + + + + +"GroupShareTitle" = "Общая история"; + +"GroupShareEnabled" = "Включено"; + +"GroupShareHint" = "Все участники увидят все сообщения группы"; + +"GroupShareMessage" = "Вы уверены что хотите сделать все сообщениям общими? Это действие необратимо."; + +"GroupShareAction" = "Сделать Общими"; + + + +"GroupEditTitle" = "Редактирование"; + +"GroupEditTitleChannel" = "Редактирование"; + +"GroupEditName" = "Название группы"; + +"GroupEditNameChannel" = "Название канала"; + +"GroupEditDescription" = "Описание"; + + + +"GroupViewMembers" = "Участники"; "GroupMemberAdmin" = "админ."; @@ -320,6 +394,8 @@ "GroupLeave" = "Покинуть группу"; +"GroupLeaveChannel" = "Leave channel"; + "GroupLeaveConfirm" = "Вы уверены, что хотите покинуть группу?"; "GroupLeaveConfirmAction" = "Покинуть"; @@ -334,24 +410,12 @@ "GroupInviteLinkRevokeAction" = "Сбросить"; -"GroupIntegrationPageTitle" = "API Интеграции"; - -"GroupIntegrationLinkTitle" = "ССЫЛКА"; -"GroupIntegrationLinkHint" = "Вы можете использовать эту ссылку для отправки сообщений из любых внешних сервисов."; - -"GroupIntegrationDoc" = "Открыть документацию"; - -"GroupIntegrationLinkRevokeMessage" = "Вы уверены, что хотите сбросить ссылку? Как только вы это сделаете, вы больше не сможете по ней отправлять сообщения в группу."; - -"GroupIntegrationLinkRevokeAction" = "Сбросить"; "GroupJoinMessage" = "Вы уверены, что хотите вступить в группу?"; "GroupJoinAction" = "Вступить"; -"GroupMembers" = "УЧАСТНИКИ"; - /* * Compose */ @@ -360,12 +424,23 @@ "CreateGroup" = "Новая группа"; +"CreateChannel" = "Новый канал"; + + "CreateGroupTitle" = "Новая группа"; "CreateGroupNamePlaceholder" = "Название группы"; "CreateGroupHint" = "Укажите название группы и поставьте фотографию"; + +"CreateChannelTitle" = "Новый канал"; + +"CreateChannelHint" = "Укажите название канала и поставьте фотографию"; + +"CreateChannelNamePlaceholder" = "Название канала"; + + "CreateGroupMembersTitle" = "Пригласить участников"; "CreateGroupMembersPlaceholders" = "Введите имена"; @@ -387,6 +462,10 @@ "ChatNoGroupAccess" = "Вы не состоите в группе"; +"ChatDeleted" = "Этот чат был удален владельцем."; + +"ChatJoin" = "Присоединиться"; + /* * Chat attachment menu */ @@ -606,6 +685,60 @@ "ActionOpenCode" = "Посмотреть код"; +"ActionMute" = "Заглушить"; + +"ActionUnmute" = "Включить оповещения"; + +"ActionDelete" = "Удалить"; + +"ActionDeleteChannel" = "Удалить канал"; + +"ActionDeleteChannelMessage" = "Стой! Удаление этого канала исключит всех подписчиков и все сообщения будут потеряны. Продолжить все равно?"; + +"ActionDeleteGroup" = "Удалить группу"; + +"ActionDeleteGroupMessage" = "Стой! Удаление этой группы исключит всех участников и все сообщения будут потеряны. Продолжить все равно?"; + +"ActionDeleteAndExit" = "Удалить и выйти"; + +"ActionClearHistory" = "Очистить историю"; + + + +"ActionDelete" = "Удалить"; + +"ActionDeleteMessage" = "Вы уверены что хотите удалить чат?"; + + +"ActionDeleteChannel" = "Удалть канал"; + +"ActionDeleteChannelMessage" = "Стой! Удаление этого канала исключит всех подписчиков и все сообщения будут потеряны. Продолжить все равно?"; + +"ActionDeleteGroup" = "Удалить группу"; + +"ActionDeleteGroupMessage" = "Стой! Удаление этой группы исключит всех участников и все сообщения будут потеряны. Продолжить все равно?"; + + +"ActionLeaveChannel" = "Покинуть канал"; + +"ActionLeaveChannelMessage" = "Вы уверены, что хотите выйти из канала?"; + +"ActionLeaveChannelAction" = "Выйти"; + + +"ActionDeleteAndExit" = "Удалить и выйти"; + +"ActionDeleteAndExitMessage" = "Вы уверены, что хотите хотите выйти из группы и удалить все сообщения?"; + +"ActionDeleteAndExitAction" = "Выйти"; + + +"ActionClearHistory" = "Очистить историю"; + +"ActionClearHistoryMessage" = "Вы уверены что хотите очистить историю?"; + +"ActionClearHistoryAction" = "Очистить"; + /* * Network */ diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Resources/zh-Hans.lproj/Localizable.strings b/actor-sdk/sdk-core-ios/ActorSDK/Resources/zh-Hans.lproj/Localizable.strings index 857ad6e9ed..b2f2714c52 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Resources/zh-Hans.lproj/Localizable.strings +++ b/actor-sdk/sdk-core-ios/ActorSDK/Resources/zh-Hans.lproj/Localizable.strings @@ -34,7 +34,7 @@ "DialogsHint" = "向左滑动查看更多选项"; -"DialogsBack" = "Chats"; +"DialogsBack" = "消息"; /* * Settings @@ -48,6 +48,13 @@ "SettingsChangeName" = "修改姓名"; +"SettingsEditHeader" = "你的姓名"; + +"SettingsEditHint" = "请输入你的真实姓名以便别人更好的认识你。"; + +"SettingsEditFieldHint" = "请输入你的姓名"; + + "SettingsUsernameNotSet" = "设置用户名"; "SettingsUsernameTitle" = "用户名"; @@ -64,7 +71,7 @@ "SettingsChangeAboutHint" = "简单介绍一下你自己"; -"SettingsSecurity" = "Privacy and Security"; +"SettingsSecurity" = "隐私和安全"; "SettingsWallpapers" = "聊天背景"; @@ -82,9 +89,9 @@ "SettingsMobilePhone" = "手机"; -"SettingsGroupChats" = "Groups"; +"SettingsGroupChats" = "群组"; -"SettingsPrivateChats" = "Private Chats"; +"SettingsPrivateChats" = "私密聊天"; /* * Notifications @@ -122,6 +129,20 @@ "NotificationsOnlyMentionsHint" = "只有当消息中提到你时才通知"; +/* + * Media + */ + +"MediaTitle" = "媒体设置"; + +"MediaPhotoDownloadHeader"= "自动下载照片"; + +"MediaAudioDownloadHeader"= "自动下载语音"; + +"MediaOtherHeader"= "其他"; + +"MediaAutoplayGif"= "自动播放GIFs图片"; + /* * Privacy */ @@ -129,22 +150,22 @@ "PrivacyTitle" = "安全"; -"PrivacyHeader" = "Privacy"; +"PrivacyHeader" = "隐私"; "PrivacyLastSeen" = "最后一次露面"; "PrivacyLastSeenHint" = "更改谁可以看到您最后一次露面时间。"; -"SettingsLastSeenEverybody" = "每个人都可见"; +"PrivacyLastSeenEverybody" = "每个人都可见"; -"SettingsLastSeenContacts" = "联系人可见"; +"PrivacyLastSeenContacts" = "联系人可见"; -"SettingsLastSeenNone" = "没人"; +"PrivacyLastSeenNone" = "没人"; -"PrivacySecurityHeader" = "Security"; +"PrivacySecurityHeader" = "安全"; -"PrivacyAllSessions" = "Active Sessions"; +"PrivacyAllSessions" = "在线的设备"; "PrivacyTerminate" = "下线所有其它登录"; @@ -194,6 +215,7 @@ "CallGroupText" = "{name}发起群组通话"; + /* * Profile */ @@ -212,11 +234,15 @@ "ProfileRemoveFromContacts" = "从通讯录中删除"; +"ProfileBlockContact" = "黑名单"; + +"ProfileUnblockContact" = "开启用户"; + "ProfileRename" = "修改联系名"; "ProfileRenameMessage" = "你可以修改通讯录中的联系人名称,此修改只体现在你的通讯录中。"; -"ProfileRenameAction" = ""; +"ProfileRenameAction" = "修改名称"; "ProfileAbout" = "自我介绍"; @@ -226,22 +252,12 @@ "ProfileTitle" = "详细资料"; -"ProfileBlockContact" = "Block User"; - -"ProfileUnblockContact" = "Unblock User"; - "CallNumber" = "拨打电话"; "CopyNumber" = "复制号码"; "NumberCopied" = "电话号码已经复制到剪贴板中"; -"SettingsEditHeader" = "姓名"; - -"SettingsEditHint" = "请输入您的真实姓名,以便于好友识别。"; - -"SettingsEditFieldHint" = "请输入姓名"; - /* * Publics */ @@ -260,6 +276,8 @@ "GroupSetTitle" = "设置群组名称"; +"GroupSetSound" = "设置群组音效"; + "GroupEditConfirm" = "确定要修改群组名称吗?"; "GroupEditConfirmAction" = "修改"; @@ -270,11 +288,82 @@ "GroupIntegrations" = "集成"; -"GroupMembers" = "{0}个成员"; +"GroupViewMembers" = "成员"; + +"GroupDescription" = "简介"; + + + +"GroupAdministration" = "管理员"; + +"GroupTypeTitle" = "群组类型"; + +"GroupTypeTitleChannel" = "频道类型"; + +"GroupPermissionsHint" = "设置群组类型"; + +"GroupPermissionsHintChannel" = "设置频道类型"; + +"GroupTypeHintPublic" = "公开群组可以被搜索并且任何人可以主动加入"; + +"GroupTypeHintPrivate" = "私密群组只有创建者可以邀请其他人加入"; + +"GroupTypeHintPublicChannel" = "公开频道可以被搜索并且任何人可以主动加入"; + +"GroupTypeHintPrivateChannel" = "私密频道只有创建者可以邀请其他人加入"; + +"GroupTypeLinkHint" = "群员可以共享此链接给其他人加入群组"; + +"GroupTypeLinkHintChannel" = "群员可以共享此链接给其他人加入频道"; + +"GroupTypePublic" = "公开的"; + +"GroupTypePrivate" = "私密的"; + +"ChannelTypePublic" = "公开的"; + +"ChannelTypePrivate" = "私密的"; + +"GroupTypePublicFull" = "公开群组"; + +"GroupTypePrivateFull" = "私密群组"; + +"ChannelTypePublicFull" = "公开频道"; + +"ChannelTypePrivateFull" = "私密频道"; + +"GroupDeleteHint" = "你将丢失所有此群组的聊天记录"; + +"GroupDeleteHintChannel" = "你将丢失所有此频道的消息记录"; + + + +"GroupShareTitle" = "公开历史记录"; + +"GroupShareEnabled" = "公开"; + +"GroupShareHint" = "所有成员将可以查看此群组的所有历史聊天记录"; + +"GroupShareMessage" = "你想公开所有历史聊天记录给所有成员吗?此操作不可逆。"; + +"GroupShareAction" = "公开"; + + + +"GroupEditTitle" = "编辑群组"; + +"GroupEditTitleChannel" = "编辑频道"; + +"GroupEditName" = "群组名称"; + +"GroupEditNameChannel" = "频道名称"; + +"GroupEditDescription" = "简介"; + "GroupMemberAdmin" = "管理员"; -"GroupMemberInfo" = "成员信息"; +"GroupMemberInfo" = "配置"; "GroupMemberMakeAdmin" = "设为群组管理员"; @@ -300,11 +389,6 @@ "GroupAddParticipantUrl" = "通过URL链接邀请加入群组"; -"GroupLeave" = "退出群组"; - -"GroupLeaveConfirm" = "你确定要退出群组吗?"; - -"GroupLeaveConfirmAction" = "退出"; "GroupInviteLinkPageTitle" = "邀请链接"; @@ -316,38 +400,37 @@ "GroupInviteLinkRevokeAction" = "重置"; -"GroupIntegrationPageTitle" = "集成API"; - -"GroupIntegrationLinkTitle" = "链接"; - -"GroupIntegrationLinkHint" = "任何外部系统都可以利用这个链接向群组发送消息。"; - -"GroupIntegrationDoc" = "打开文档"; -"GroupIntegrationLinkRevokeMessage" = "确定要重置这个链接,重置后其他系统将无法使用这个链接发送消息。"; - -"GroupIntegrationLinkRevokeAction" = "重置"; "GroupJoinMessage" = "确定要加入群组吗?"; "GroupJoinAction" = "加入"; -"GroupMembers" = "群组成员"; - /* * Compose */ "ComposeTitle" = "新消息"; + "CreateGroup" = "创建群组"; +"CreateChannel" = "创建频道"; + + "CreateGroupTitle" = "创建群组"; "CreateGroupHint" = "请填写群组名称和图标"; "CreateGroupNamePlaceholder" = "输入群组名称"; +"CreateChannelTitle" = "创建频道"; + +"CreateChannelHint" = "请输入频道名称并设置频道图标"; + +"CreateChannelNamePlaceholder" = "频道名称"; + + "CreateGroupMembersTitle" = "邀请成员"; "CreateGroupMembersPlaceholders" = "输入名称"; @@ -380,6 +463,10 @@ "Chat.MicrophoneAccessDisabled" = "我们需要访问麦克风来发送语音信息. 请打开 设置 — 隐私 — 麦克风,然后打开开关"; +"ChatDeleted" = "此聊天已被发起者删除"; + +"ChatJoin" = "加入"; + /* * Chat attachment menu */ @@ -474,12 +561,12 @@ "AuthPhoneTitle" = "请输入你的手机号码"; +"AuthPhoneUseEmail" = "使用邮件"; + "AuthPhoneHint" = "为了保障你的个人信息安全,需要验证你的手机号码。"; "AuthPhonePlaceholder" = "手机号码"; -"AuthPhoneUseEmail" = "使用邮件"; - "AuthEmailTitle" = "请输入你的电子邮件"; "AuthEmailHint" = "不会向你发送任何垃圾邮件。"; @@ -512,6 +599,7 @@ "AuthOTPCallHint" = "{app_name}将在{time}秒后拨打手机告知验证码"; + /* * Common Elements */ @@ -556,6 +644,8 @@ * Alerts */ +"AlertUnblock" = "取消黑名单"; + "AlertCancel" = "取消"; "AlertNext" = "下一步"; @@ -578,7 +668,6 @@ "UnsupportedContent" = "这个版本不支持此消息。请等待应用升级,之后就可以看到消息了。"; - /* * Actions */ @@ -601,6 +690,47 @@ "ActionOpenCode" = "查看代码"; +"ActionMute" = "禁言"; + +"ActionUnmute" = "取消禁言"; + + +"ActionDelete" = "删除"; + +"ActionDeleteMessage" = "确定删除聊天吗?"; + + +"ActionDeleteChannel" = "删除频道"; + +"ActionDeleteChannelMessage" = "删除此频道将清空成员和消息记录,确定要这么做吗?"; + +"ActionDeleteGroup" = "删除群组"; + +"ActionDeleteGroupMessage" = "删除此群组将清空成员和消息记录,确定要这么做吗?"; + + +"ActionLeaveChannel" = "离开频道"; + +"ActionLeaveChannelMessage" = "确定离开频道吗?"; + +"ActionLeaveChannelAction" = "离开"; + + + +"ActionDeleteAndExit" = "删除并退出"; + +"ActionDeleteAndExitMessage" = "确定退出群组并删除消息吗?"; + +"ActionDeleteAndExitAction" = "退出"; + + + +"ActionClearHistory" = "清除历史"; + +"ActionClearHistoryMessage" = "确定清除所有历史吗?"; + +"ActionClearHistoryAction" = "清除"; + /* * Network */ @@ -622,4 +752,3 @@ "ErrorUnableToJoin" = "无法加入群组"; "ErrorUnableToCall" = "无法拨打这个号码"; - diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorApplicationDelegate.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorApplicationDelegate.swift index 9f717d9083..491e627e6c 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorApplicationDelegate.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorApplicationDelegate.swift @@ -4,7 +4,7 @@ import Foundation -public class ActorApplicationDelegate: ActorSDKDelegateDefault, UIApplicationDelegate { +open class ActorApplicationDelegate: ActorSDKDelegateDefault, UIApplicationDelegate { public override init() { super.init() @@ -12,57 +12,57 @@ public class ActorApplicationDelegate: ActorSDKDelegateDefault, UIApplicationDel ActorSDK.sharedActor().delegate = self } - public func applicationDidFinishLaunching(application: UIApplication) { + open func applicationDidFinishLaunching(_ application: UIApplication) { ActorSDK.sharedActor().applicationDidFinishLaunching(application) } - public func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool { + open func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { ActorSDK.sharedActor().applicationDidFinishLaunching(application) return true } - public func applicationDidBecomeActive(application: UIApplication) { + open func applicationDidBecomeActive(_ application: UIApplication) { ActorSDK.sharedActor().applicationDidBecomeActive(application) } - public func applicationWillEnterForeground(application: UIApplication) { + open func applicationWillEnterForeground(_ application: UIApplication) { ActorSDK.sharedActor().applicationWillEnterForeground(application) } - public func applicationDidEnterBackground(application: UIApplication) { + open func applicationDidEnterBackground(_ application: UIApplication) { ActorSDK.sharedActor().applicationDidEnterBackground(application) } - public func applicationWillResignActive(application: UIApplication) { + open func applicationWillResignActive(_ application: UIApplication) { ActorSDK.sharedActor().applicationWillResignActive(application) } - public func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) { + open func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) { ActorSDK.sharedActor().application(application, didReceiveRemoteNotification: userInfo) } - public func application(application: UIApplication, didRegisterUserNotificationSettings notificationSettings: UIUserNotificationSettings) { + open func application(_ application: UIApplication, didRegister notificationSettings: UIUserNotificationSettings) { ActorSDK.sharedActor().application(application, didRegisterUserNotificationSettings: notificationSettings) } - public func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) { + open func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let tokenString = "\(deviceToken)".replace(" ", dest: "").replace("<", dest: "").replace(">", dest: "") ActorSDK.sharedActor().pushRegisterToken(tokenString) } - public func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) { + open func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { ActorSDK.sharedActor().application(application, didReceiveRemoteNotification: userInfo, fetchCompletionHandler: completionHandler) } - public func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) { + open func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { ActorSDK.sharedActor().application(application, performFetchWithCompletionHandler: completionHandler) } - public func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool { - return ActorSDK.sharedActor().application(application, openURL: url, sourceApplication: sourceApplication, annotation: annotation) + open func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool { + return ActorSDK.sharedActor().application(application, openURL: url, sourceApplication: sourceApplication, annotation: annotation as AnyObject) } - public func application(application: UIApplication, handleOpenURL url: NSURL) -> Bool { + open func application(_ application: UIApplication, handleOpen url: URL) -> Bool { return ActorSDK.sharedActor().application(application, handleOpenURL: url) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/ActorCoreExt.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/ActorCoreExt.swift index cc5203531a..227689bf8b 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/ActorCoreExt.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/ActorCoreExt.swift @@ -13,41 +13,41 @@ public var Actor : ACCocoaMessenger { public extension ACCocoaMessenger { - public func sendUIImage(image: NSData, peer: ACPeer, animated:Bool) { + public func sendUIImage(_ image: Data, peer: ACPeer, animated:Bool) { let imageFromData = UIImage(data:image) let thumb = imageFromData!.resizeSquare(90, maxH: 90); let resized = imageFromData!.resizeOptimize(1200 * 1200); let thumbData = UIImageJPEGRepresentation(thumb, 0.55); - let fastThumb = ACFastThumb(int: jint(thumb.size.width), withInt: jint(thumb.size.height), withByteArray: thumbData!.toJavaBytes()) + let fastThumb = ACFastThumb(int: jint(thumb.size.width), with: jint(thumb.size.height), with: thumbData!.toJavaBytes()) - let descriptor = "/tmp/"+NSUUID().UUIDString + let descriptor = "/tmp/"+UUID().uuidString let path = CocoaFiles.pathFromDescriptor(descriptor); - animated ? image.writeToFile(path, atomically: true) : UIImageJPEGRepresentation(resized, 0.80)!.writeToFile(path, atomically: true) + animated ? ((try? image.write(to: URL(fileURLWithPath: path), options: [.atomic])) != nil) : ((try? UIImageJPEGRepresentation(resized, 0.80)!.write(to: URL(fileURLWithPath: path), options: [.atomic])) != nil) - animated ? sendAnimationWithPeer(peer, withName: "image.gif", withW: jint(resized.size.width), withH:jint(resized.size.height), withThumb: fastThumb, withDescriptor: descriptor) : sendPhotoWithPeer(peer, withName: "image.jpg", withW: jint(resized.size.width), withH: jint(resized.size.height), withThumb: fastThumb, withDescriptor: descriptor) + animated ? sendAnimation(with: peer, withName: "image.gif", withW: jint(resized.size.width), withH:jint(resized.size.height), with: fastThumb, withDescriptor: descriptor) : sendPhoto(with: peer, withName: "image.jpg", withW: jint(resized.size.width), withH: jint(resized.size.height), with: fastThumb, withDescriptor: descriptor) } - public func sendVideo(url: NSURL, peer: ACPeer) { + public func sendVideo(_ url: URL, peer: ACPeer) { - if let videoData = NSData(contentsOfURL: url) { // if data have on this local path url go to upload + if let videoData = try? Data(contentsOf: url) { // if data have on this local path url go to upload - let descriptor = "/tmp/"+NSUUID().UUIDString + let descriptor = "/tmp/"+UUID().uuidString let path = CocoaFiles.pathFromDescriptor(descriptor); - videoData.writeToFile(path, atomically: true) // write to file + try? videoData.write(to: URL(fileURLWithPath: path), options: [.atomic]) // write to file // get video duration - let assetforduration = AVURLAsset(URL: url) + let assetforduration = AVURLAsset(url: url) let videoDuration = assetforduration.duration let videoDurationSeconds = CMTimeGetSeconds(videoDuration) // get thubnail and upload - let movieAsset = AVAsset(URL: url) // video asset + let movieAsset = AVAsset(url: url) // video asset let imageGenerator = AVAssetImageGenerator(asset: movieAsset) var thumbnailTime = movieAsset.duration thumbnailTime.value = 25 @@ -55,8 +55,8 @@ public extension ACCocoaMessenger { let orientation = movieAsset.videoOrientation() do { - let imageRef = try imageGenerator.copyCGImageAtTime(thumbnailTime, actualTime: nil) - let thumbnail = UIImage(CGImage: imageRef) + let imageRef = try imageGenerator.copyCGImage(at: thumbnailTime, actualTime: nil) + let thumbnail = UIImage(cgImage: imageRef) var thumb = thumbnail.resizeSquare(90, maxH: 90); let resized = thumbnail.resizeOptimize(1200 * 1200); @@ -65,7 +65,7 @@ public extension ACCocoaMessenger { } let thumbData = UIImageJPEGRepresentation(thumb, 0.55); // thumbnail binary data - let fastThumb = ACFastThumb(int: jint(resized.size.width), withInt: jint(resized.size.height), withByteArray: thumbData!.toJavaBytes()) + let fastThumb = ACFastThumb(int: jint(resized.size.width), with: jint(resized.size.height), with: thumbData!.toJavaBytes()) print("video upload imageRef = \(imageRef)") print("video upload thumbnail = \(thumbnail)") @@ -76,9 +76,9 @@ public extension ACCocoaMessenger { print("video upload height = \(thumbnail.size.height)") if (orientation.orientation.isPortrait == true) { - sendVideoWithPeer(peer, withName: "video.mp4", withW: jint(thumbnail.size.height/2), withH: jint(thumbnail.size.width/2), withDuration: jint(videoDurationSeconds), withThumb: fastThumb, withDescriptor: descriptor) + self.sendVideo(with: peer, withName: "video.mp4", withW: jint(thumbnail.size.height/2), withH: jint(thumbnail.size.width/2), withDuration: jint(videoDurationSeconds), with: fastThumb, withDescriptor: descriptor) } else { - sendVideoWithPeer(peer, withName: "video.mp4", withW: jint(thumbnail.size.width), withH: jint(thumbnail.size.height), withDuration: jint(videoDurationSeconds), withThumb: fastThumb, withDescriptor: descriptor) + self.sendVideo(with: peer, withName: "video.mp4", withW: jint(thumbnail.size.width), withH: jint(thumbnail.size.height), withDuration: jint(videoDurationSeconds), with: fastThumb, withDescriptor: descriptor) } @@ -91,28 +91,30 @@ public extension ACCocoaMessenger { } - private func prepareAvatar(image: UIImage) -> String { - let res = "/tmp/" + NSUUID().UUIDString + fileprivate func prepareAvatar(_ image: UIImage) -> String { + let res = "/tmp/" + UUID().uuidString let avatarPath = CocoaFiles.pathFromDescriptor(res) let thumb = image.resizeSquare(800, maxH: 800); - UIImageJPEGRepresentation(thumb, 0.8)!.writeToFile(avatarPath, atomically: true) + try? UIImageJPEGRepresentation(thumb, 0.8)!.write(to: URL(fileURLWithPath: avatarPath), options: [.atomic]) return res } - public func changeOwnAvatar(image: UIImage) { - changeMyAvatarWithDescriptor(prepareAvatar(image)) + public func changeOwnAvatar(_ image: UIImage) { + changeMyAvatar(withDescriptor: prepareAvatar(image)) } - public func changeGroupAvatar(gid: jint, image: UIImage) { - changeGroupAvatarWithGid(gid, withDescriptor: prepareAvatar(image)) + public func changeGroupAvatar(_ gid: jint, image: UIImage) -> String { + let fileName = prepareAvatar(image) + self.changeGroupAvatar(withGid: gid, withDescriptor: fileName) + return fileName } - public func requestFileState(fileId: jlong, notDownloaded: (()->())?, onDownloading: ((progress: Double) -> ())?, onDownloaded: ((reference: String) -> ())?) { - Actor.requestStateWithFileId(fileId, withCallback: AAFileCallback(notDownloaded: notDownloaded, onDownloading: onDownloading, onDownloaded: onDownloaded)) + public func requestFileState(_ fileId: jlong, notDownloaded: (()->())?, onDownloading: ((_ progress: Double) -> ())?, onDownloaded: ((_ reference: String) -> ())?) { + Actor.requestState(withFileId: fileId, with: AAFileCallback(notDownloaded: notDownloaded, onDownloading: onDownloading, onDownloaded: onDownloaded)) } - public func requestFileState(fileId: jlong, onDownloaded: ((reference: String) -> ())?) { - Actor.requestStateWithFileId(fileId, withCallback: AAFileCallback(notDownloaded: nil, onDownloading: nil, onDownloaded: onDownloaded)) + public func requestFileState(_ fileId: jlong, onDownloaded: ((_ reference: String) -> ())?) { + Actor.requestState(withFileId: fileId, with: AAFileCallback(notDownloaded: nil, onDownloading: nil, onDownloaded: onDownloaded)) } } @@ -120,10 +122,10 @@ public extension ACCocoaMessenger { // Collcections // -extension JavaUtilAbstractCollection : SequenceType { +extension JavaUtilAbstractCollection : Sequence { - public func generate() -> NSFastGenerator { - return NSFastGenerator(self) + public func makeIterator() -> NSFastEnumerationIterator { + return NSFastEnumerationIterator(self) } } @@ -132,7 +134,7 @@ public extension JavaUtilList { public func toSwiftArray() -> [T] { var res = [T]() for i in 0..() -> [T] { var res = [T]() for i in 0.. IOSByteArray { - return IOSByteArray(bytes: UnsafePointer(self.bytes), count: UInt(self.length)) + return IOSByteArray(bytes: (self as NSData).bytes.bindMemory(to: jbyte.self, capacity: self.count), count: UInt(self.count)) } } @@ -162,13 +164,13 @@ public extension ACPeer { public var isGroup: Bool { get { - return self.peerType.ordinal() == ACPeerType.GROUP().ordinal() + return self.peerType.ordinal() == ACPeerType.group().ordinal() } } public var isPrivate: Bool { get { - return self.peerType.ordinal() == ACPeerType.PRIVATE().ordinal() + return self.peerType.ordinal() == ACPeerType.private().ordinal() } } } @@ -186,35 +188,35 @@ public extension ACMessage { // Callbacks // -public class AACommandCallback: NSObject, ACCommandCallback { +open class AACommandCallback: NSObject, ACCommandCallback { - public var resultClosure: ((val: AnyObject!) -> ())?; - public var errorClosure: ((val:JavaLangException!) -> ())?; + open var resultClosure: ((_ val: Any?) -> ())?; + open var errorClosure: ((_ val:JavaLangException?) -> ())?; - public init(result: ((val:T?) -> ())?, error: ((val:JavaLangException!) -> ())?) { + public init(result: ((_ val:T?) -> ())?, error: ((_ val:JavaLangException?) -> ())?) { super.init() - self.resultClosure = { (val: AnyObject!) -> () in - result?(val: val as? T) + self.resultClosure = { (val: Any!) -> () in + (result?(val as? T))! } self.errorClosure = error } - public func onResult(res: AnyObject!) { - resultClosure?(val: res) + open func onResult(_ res: Any!) { + resultClosure?(res) } - public func onError(e: JavaLangException!) { - errorClosure?(val: e) + open func onError(_ e: JavaLangException!) { + errorClosure?(e) } } class AAUploadFileCallback : NSObject, ACUploadFileCallback { let notUploaded: (()->())? - let onUploading: ((progress: Double) -> ())? + let onUploading: ((_ progress: Double) -> ())? let onUploadedClosure: (() -> ())? - init(notUploaded: (()->())?, onUploading: ((progress: Double) -> ())?, onUploadedClosure: (() -> ())?) { + init(notUploaded: (()->())?, onUploading: ((_ progress: Double) -> ())?, onUploadedClosure: (() -> ())?) { self.onUploading = onUploading self.notUploaded = notUploaded self.onUploadedClosure = onUploadedClosure; @@ -228,24 +230,24 @@ class AAUploadFileCallback : NSObject, ACUploadFileCallback { self.onUploadedClosure?() } - func onUploading(progress: jfloat) { - self.onUploading?(progress: Double(progress)) + func onUploading(_ progress: jfloat) { + self.onUploading?(Double(progress)) } } class AAFileCallback : NSObject, ACFileCallback { let notDownloaded: (()->())? - let onDownloading: ((progress: Double) -> ())? - let onDownloaded: ((fileName: String) -> ())? + let onDownloading: ((_ progress: Double) -> ())? + let onDownloaded: ((_ fileName: String) -> ())? - init(notDownloaded: (()->())?, onDownloading: ((progress: Double) -> ())?, onDownloaded: ((reference: String) -> ())?) { + init(notDownloaded: (()->())?, onDownloading: ((_ progress: Double) -> ())?, onDownloaded: ((_ reference: String) -> ())?) { self.notDownloaded = notDownloaded; self.onDownloading = onDownloading; self.onDownloaded = onDownloaded; } - init(onDownloaded: (reference: String) -> ()) { + init(onDownloaded: @escaping (_ reference: String) -> ()) { self.notDownloaded = nil; self.onDownloading = nil; self.onDownloaded = onDownloaded; @@ -255,12 +257,12 @@ class AAFileCallback : NSObject, ACFileCallback { self.notDownloaded?(); } - func onDownloading(progress: jfloat) { - self.onDownloading?(progress: Double(progress)); + func onDownloading(_ progress: jfloat) { + self.onDownloading?(Double(progress)); } - func onDownloaded(reference: ARFileSystemReference!) { - self.onDownloaded?(fileName: reference!.getDescriptor()); + func onDownloaded(_ reference: ARFileSystemReference!) { + self.onDownloaded?(reference!.getDescriptor()); } } @@ -269,26 +271,13 @@ class AAFileCallback : NSObject, ACFileCallback { // Markdown // -//public class ARMDFormattedText { -// -// public let isTrivial: Bool -// public let attributedText: NSAttributedString -// public let code: [String] -// -// public init(attributedText: NSAttributedString, isTrivial: Bool, code: [String]) { -// self.attributedText = attributedText -// self.code = code -// self.isTrivial = isTrivial -// } -//} - -public class TextParser { +open class TextParser { - public let textColor: UIColor - public let linkColor: UIColor - public let fontSize: CGFloat + open let textColor: UIColor + open let linkColor: UIColor + open let fontSize: CGFloat - private let markdownParser = ARMarkdownParser(int: ARMarkdownParser_MODE_FULL) + fileprivate let markdownParser = ARMarkdownParser(int: ARMarkdownParser_MODE_FULL) public init(textColor: UIColor, linkColor: UIColor, fontSize: CGFloat) { self.textColor = textColor @@ -296,10 +285,10 @@ public class TextParser { self.fontSize = fontSize } - public func parse(text: String) -> ParsedText { - let doc = markdownParser.processDocumentWithNSString(text) + open func parse(_ text: String) -> ParsedText { + let doc = markdownParser?.processDocument(with: text) - if doc.isTrivial() { + if (doc?.isTrivial())! { let nAttrText = NSMutableAttributedString(string: text) let range = NSRange(location: 0, length: nAttrText.length) nAttrText.yy_setColor(textColor, range: range) @@ -309,12 +298,12 @@ public class TextParser { var sources = [String]() - let sections: [ARMDSection] = doc.getSections().toSwiftArray() + let sections: [ARMDSection] = doc!.getSections().toSwiftArray() let nAttrText = NSMutableAttributedString() var isFirst = true for s in sections { if !isFirst { - nAttrText.appendAttributedString(NSAttributedString(string: "\n")) + nAttrText.append(NSAttributedString(string: "\n")) } isFirst = false @@ -329,13 +318,13 @@ public class TextParser { str.yy_setFont(UIFont.textFontOfSize(fontSize), range: range) str.yy_setColor(linkColor, range: range) - nAttrText.appendAttributedString(str) + nAttrText.append(str) sources.append(s.getCode().getCode()) } else if s.getType() == ARMDSection_TYPE_TEXT { let child: [ARMDText] = s.getText().toSwiftArray() for c in child { - nAttrText.appendAttributedString(buildText(c, fontSize: fontSize)) + nAttrText.append(buildText(c, fontSize: fontSize)) } } else { fatalError("Unsupported section type") @@ -345,7 +334,7 @@ public class TextParser { return ParsedText(attributedText: nAttrText, isTrivial: false, code: sources) } - private func buildText(text: ARMDText, fontSize: CGFloat) -> NSAttributedString { + fileprivate func buildText(_ text: ARMDText, fontSize: CGFloat) -> NSAttributedString { if let raw = text as? ARMDRawText { let res = NSMutableAttributedString(string: raw.getRawText()) let range = NSRange(location: 0, length: res.length) @@ -361,14 +350,14 @@ public class TextParser { // Processing child texts let child: [ARMDText] = span.getChild().toSwiftArray() for c in child { - res.appendAttributedString(buildText(c, fontSize: fontSize)) + res.append(buildText(c, fontSize: fontSize)) } // Setting span elements - if span.getSpanType() == ARMDSpan_TYPE_BOLD { - res.appendFont(UIFont.boldSystemFontOfSize(fontSize)) - } else if span.getSpanType() == ARMDSpan_TYPE_ITALIC { - res.appendFont(UIFont.italicSystemFontOfSize(fontSize)) + if span.getType() == ARMDSpan_TYPE_BOLD { + res.appendFont(UIFont.boldSystemFont(ofSize: fontSize)) + } else if span.getType() == ARMDSpan_TYPE_ITALIC { + res.appendFont(UIFont.italicSystemFont(ofSize: fontSize)) } else { fatalError("Unsupported span type") } @@ -378,9 +367,9 @@ public class TextParser { } else if let url = text as? ARMDUrl { // Parsing url element - let nsUrl = NSURL(string: url.getUrl()) + let nsUrl = URL(string: url.getUrl()) if nsUrl != nil { - let res = NSMutableAttributedString(string: url.getUrlTitle()) + let res = NSMutableAttributedString(string: url.getTitle()) let range = NSRange(location: 0, length: res.length) let highlight = YYTextHighlight() highlight.userInfo = ["url" : url.getUrl()] @@ -390,7 +379,7 @@ public class TextParser { return res } else { // Unable to parse: show as text - let res = NSMutableAttributedString(string: url.getUrlTitle()) + let res = NSMutableAttributedString(string: url.getTitle()) let range = NSRange(location: 0, length: res.length) res.beginEditing() res.yy_setFont(UIFont.textFontOfSize(fontSize), range: range) @@ -404,11 +393,11 @@ public class TextParser { } } -public class ParsedText { +open class ParsedText { - public let isTrivial: Bool - public let attributedText: NSAttributedString - public let code: [String] + open let isTrivial: Bool + open let attributedText: NSAttributedString + open let code: [String] public init(attributedText: NSAttributedString, isTrivial: Bool, code: [String]) { self.attributedText = attributedText @@ -416,134 +405,25 @@ public class ParsedText { self.isTrivial = isTrivial } } -// -//public extension ARMarkdownParser { -// -// public func parse(text: String, textColor: UIColor, fontSize: CGFloat) -> ARMDFormattedText { -// -// let doc = self.processDocumentWithNSString(text) -// if doc.isTrivial() { -// let nAttrText = NSMutableAttributedString(string: text) -// let range = NSRange(location: 0, length: nAttrText.length) -// nAttrText.yy_setColor(textColor, range: range) -// nAttrText.yy_setFont(UIFont.textFontOfSize(fontSize), range: range) -// return ARMDFormattedText(attributedText: nAttrText, isTrivial: true, code: []) -// } -// -// var sources = [String]() -// -// let sections: [ARMDSection] = doc.getSections().toSwiftArray() -// let nAttrText = NSMutableAttributedString() -// var isFirst = true -// for s in sections { -// if !isFirst { -// nAttrText.appendAttributedString(NSAttributedString(string: "\n")) -// } -// isFirst = false -// -// if s.getType() == ARMDSection_TYPE_CODE { -// let attributes = [NSLinkAttributeName: NSURL(string: "source:///\(sources.count)") as! AnyObject, -// NSFontAttributeName: UIFont.textFontOfSize(fontSize)] -// nAttrText.appendAttributedString(NSAttributedString(string: "Open Code", attributes: attributes)) -// sources.append(s.getCode().getCode()) -// } else if s.getType() == ARMDSection_TYPE_TEXT { -// let child: [ARMDText] = s.getText().toSwiftArray() -// for c in child { -// nAttrText.appendAttributedString(buildText(c, fontSize: fontSize)) -// } -// } else { -// fatalError("Unsupported section type") -// } -// } -// -//// let range = NSRange(location: 0, length: nAttrText.length) -// -//// nAttrText.yy_setColor(textColor, range: range) -//// nAttrText.yy_setFont(UIFont.textFontOfSize(fontSize), range: range) -// -//// nAttrText.enumerateAttributesInRange(range, options: NSAttributedStringEnumerationOptions.LongestEffectiveRangeNotRequired) { (attrs, range, objBool) -> Void in -//// var attributeDictionary = NSDictionary(dictionary: attrs) -//// -//// for k in attributeDictionary.allKeys { -//// let v = attributeDictionary.objectForKey(k) -//// -//// print("attr: \(k) -> \(v) at \(range)") -//// } -//// } -//// -// return ARMDFormattedText(attributedText: nAttrText, isTrivial: false, code: sources) -// } -// -// private func buildText(text: ARMDText, fontSize: CGFloat) -> NSAttributedString { -// if let raw = text as? ARMDRawText { -//// let res = NSMutableAttributedString(string: raw.getRawText()) -//// res.yy_setFont(UIFont.textFontOfSize(fontSize), range: NSRange(location: 0, length: raw.getRawText().length)) -//// return res -// return NSAttributedString(string: raw.getRawText(), font: UIFont.textFontOfSize(fontSize)) -// } else if let span = text as? ARMDSpan { -// let res = NSMutableAttributedString() -// res.beginEditing() -// -// // Processing child texts -// let child: [ARMDText] = span.getChild().toSwiftArray() -// for c in child { -// res.appendAttributedString(buildText(c, fontSize: fontSize)) -// } -// -// // Setting span elements -// if span.getSpanType() == ARMDSpan_TYPE_BOLD { -// res.appendFont(UIFont.boldSystemFontOfSize(fontSize)) -// } else if span.getSpanType() == ARMDSpan_TYPE_ITALIC { -// res.appendFont(UIFont.italicSystemFontOfSize(fontSize)) -// } else { -// fatalError("Unsupported span type") -// } -// -// res.endEditing() -// return res -// } else if let url = text as? ARMDUrl { -// -// // Parsing url element -// let nsUrl = NSURL(string: url.getUrl()) -// if nsUrl != nil { -// let res = NSMutableAttributedString(string: url.getUrlTitle()) -// let range = NSRange(location: 0, length: res.length) -// let highlight = YYTextHighlight() -//// res.yy_setFont(UIFont.textFontOfSize(fontSize), range: range) -//// res.yy_setTextHighlightRange(range, color: UIColor.redColor(), backgroundColor: nil, tapAction: nil) -// // res.yy_setColor(UIColor.greenColor(), range: range) -// return res -//// let attributes = [NSLinkAttributeName: nsUrl as! AnyObject, -//// NSFontAttributeName: UIFont.textFontOfSize(fontSize)] -//// return NSAttributedString(string: url.getUrlTitle(), attributes: attributes) -// } else { -// // Unable to parse: show as text -// return NSAttributedString(string: url.getUrlTitle(), font: UIFont.textFontOfSize(fontSize)) -// } -// } else { -// fatalError("Unsupported text type") -// } -// } -//} -// + // // Promises // -public class AAPromiseFunc: NSObject, ARPromiseFunc { +open class AAPromiseFunc: NSObject, ARPromiseFunc { - let closure: (resolver: ARPromiseResolver) -> () - init(closure: (resolver: ARPromiseResolver) -> ()){ + let closure: (_ resolver: ARPromiseResolver) -> () + init(closure: @escaping (_ resolver: ARPromiseResolver) -> ()){ self.closure = closure } - public func exec(resolver: ARPromiseResolver) { - closure(resolver: resolver) + open func exec(_ resolver: ARPromiseResolver) { + closure(resolver) } } extension ARPromise { - convenience init(closure: (resolver: ARPromiseResolver) -> ()) { + convenience init(closure: @escaping (_ resolver: ARPromiseResolver) -> ()) { self.init(executor: AAPromiseFunc(closure: closure)) } } @@ -552,77 +432,77 @@ extension ARPromise { // Data Binding // -public class AABinder { +open class AABinder { - private var bindings : [BindHolder] = [] + fileprivate var bindings : [BindHolder] = [] public init() { } - public func bind(valueModel1:ARValue, valueModel2:ARValue, valueModel3:ARValue, closure: (value1:T1!, value2:T2!, value3:T3!) -> ()) { + open func bind(_ valueModel1:ARValue, valueModel2:ARValue, valueModel3:ARValue, closure: @escaping (_ value1:T1?, _ value2:T2?, _ value3:T3?) -> ()) { let listener1 = BindListener { (_value1) -> () in - closure(value1: _value1 as? T1, value2: valueModel2.get() as? T2, value3: valueModel3.get() as? T3) + closure(_value1 as? T1, valueModel2.get() as? T2, valueModel3.get() as? T3) }; let listener2 = BindListener { (_value2) -> () in - closure(value1: valueModel1.get() as? T1, value2: _value2 as? T2, value3: valueModel3.get() as? T3) + closure(valueModel1.get() as? T1, _value2 as? T2, valueModel3.get() as? T3) }; let listener3 = BindListener { (_value3) -> () in - closure(value1: valueModel1.get() as? T1, value2: valueModel2.get() as? T2, value3: _value3 as? T3) + closure(valueModel1.get() as? T1, valueModel2.get() as? T2, _value3 as? T3) }; bindings.append(BindHolder(valueModel: valueModel1, listener: listener1)) bindings.append(BindHolder(valueModel: valueModel2, listener: listener2)) bindings.append(BindHolder(valueModel: valueModel3, listener: listener3)) - valueModel1.subscribeWithListener(listener1, notify: false) - valueModel2.subscribeWithListener(listener2, notify: false) - valueModel3.subscribeWithListener(listener3, notify: false) - closure(value1: valueModel1.get() as? T1, value2: valueModel2.get() as? T2, value3: valueModel3.get() as? T3) + valueModel1.subscribe(with: listener1, notify: false) + valueModel2.subscribe(with: listener2, notify: false) + valueModel3.subscribe(with: listener3, notify: false) + closure(valueModel1.get() as? T1, valueModel2.get() as? T2, valueModel3.get() as? T3) } - public func bind(valueModel1:ARValue, valueModel2:ARValue, closure: (value1:T1!, value2:T2!) -> ()) { + open func bind(_ valueModel1:ARValue, valueModel2:ARValue, closure: @escaping (_ value1:T1?, _ value2:T2?) -> ()) { let listener1 = BindListener { (_value1) -> () in - closure(value1: _value1 as? T1, value2: valueModel2.get() as? T2) + closure(_value1 as? T1, valueModel2.get() as? T2) }; let listener2 = BindListener { (_value2) -> () in - closure(value1: valueModel1.get() as? T1, value2: _value2 as? T2) + closure(valueModel1.get() as? T1, _value2 as? T2) }; bindings.append(BindHolder(valueModel: valueModel1, listener: listener1)) bindings.append(BindHolder(valueModel: valueModel2, listener: listener2)) - valueModel1.subscribeWithListener(listener1, notify: false) - valueModel2.subscribeWithListener(listener2, notify: false) - closure(value1: valueModel1.get() as? T1, value2: valueModel2.get() as? T2) + valueModel1.subscribe(with: listener1, notify: false) + valueModel2.subscribe(with: listener2, notify: false) + closure(valueModel1.get() as? T1, valueModel2.get() as? T2) } - public func bind(value:ARValue, closure: (value: T!)->()) { + open func bind(_ value:ARValue, closure: @escaping (_ value: T?)->()) { let listener = BindListener { (value2) -> () in - closure(value: value2 as? T) + closure(value2 as? T) }; let holder = BindHolder(valueModel: value, listener: listener) bindings.append(holder) - value.subscribeWithListener(listener) + value.subscribe(with: listener) } - public func unbindAll() { + open func unbindAll() { for holder in bindings { - holder.valueModel.unsubscribeWithListener(holder.listener) + holder.valueModel.unsubscribe(with: holder.listener) } - bindings.removeAll(keepCapacity: true) + bindings.removeAll(keepingCapacity: true) } } class BindListener: NSObject, ARValueChangedListener { - var closure: ((value: AnyObject?)->())? + var closure: ((_ value: Any?)->())? - init(closure: (value: AnyObject?)->()) { + init(closure: @escaping (_ value: Any?)->()) { self.closure = closure } - func onChanged(val: AnyObject!, withModel valueModel: ARValue!) { - closure?(value: val) + func onChanged(_ val: Any!, withModel valueModel: ARValue!) { + closure?(val) } } @@ -634,4 +514,4 @@ class BindHolder { self.valueModel = valueModel self.listener = listener } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/ActorRuntime.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/ActorRuntime.swift index 518f7dc771..e40d5ddd4b 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/ActorRuntime.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/ActorRuntime.swift @@ -6,7 +6,7 @@ import Foundation class AAActorRuntime { - private static var isInited = false + fileprivate static var isInited = false class func configureRuntime() { if isInited { @@ -18,11 +18,11 @@ class AAActorRuntime { ARCocoaHttpProxyProvider.setHttpRuntime(CocoaHttpRuntime()) ARCocoaFileSystemProxyProvider.setFileSystemRuntime(CocoaFileSystemRuntime()) ARCocoaNetworkProxyProvider.setNetworkRuntime(CocoaNetworkRuntime()) - ARCocoaAssetsProxyProvider.setAssetsRuntimeWithARAssetsRuntime(CocoaAssetsRuntime()) + ARCocoaAssetsProxyProvider.setAssetsRuntimeWith(CocoaAssetsRuntime()) ARCocoaWebRTCProxyProvider.setWebRTCRuntime(CocoaWebRTCRuntime()) ARCocoaLifecycleProxyProvider.setLifecycleRuntime(CocoaLifecycleRuntime()) ARCocoaDispatcher.setDispatcherProxy(CocoaDispatcher()) // ARRuntimeEnvironment.setIsProductionWithBoolean(true) - ARCocoaCryptoProvider.setProxyProviderWithARCocoaCryptoProxyProvider(CocoaCrypto()) + ARCocoaCryptoProvider.setProxyProviderWithARCocoaCryptoProxy(CocoaCrypto()) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaAssetsRuntime.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaAssetsRuntime.swift index 0f8fcf6e02..152e2bc8f9 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaAssetsRuntime.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaAssetsRuntime.swift @@ -6,19 +6,19 @@ import Foundation class CocoaAssetsRuntime: NSObject, ARAssetsRuntime { - func hasAssetWithNSString(name: String!) -> jboolean { - if NSBundle.mainBundle().pathForResource(name, ofType: nil) != nil { + func hasAsset(with name: String!) -> jboolean { + if Bundle.main.path(forResource: name, ofType: nil) != nil { return true } - if NSBundle.framework.pathForResource(name, ofType: nil) != nil { + if Bundle.framework.path(forResource: name, ofType: nil) != nil { return true } return false } - func loadAssetWithNSString(name: String!) -> String! { + func loadAsset(with name: String!) -> String! { var path: String? - path = NSBundle.mainBundle().pathForResource(name, ofType: nil) + path = Bundle.main.path(forResource: name, ofType: nil) if path != nil { do { return try String(contentsOfFile: path!) @@ -27,7 +27,7 @@ class CocoaAssetsRuntime: NSObject, ARAssetsRuntime { } } - path = NSBundle.framework.pathForResource(name, ofType: nil) + path = Bundle.framework.path(forResource: name, ofType: nil) if path != nil { do { return try String(contentsOfFile: path!) @@ -39,22 +39,22 @@ class CocoaAssetsRuntime: NSObject, ARAssetsRuntime { return nil } - func loadBinAssetWithNSString(name: String!) -> IOSByteArray! { + func loadBinAsset(with name: String!) -> IOSByteArray! { var path: String? - path = NSBundle.mainBundle().pathForResource(name, ofType: nil) + path = Bundle.main.path(forResource: name, ofType: nil) if path != nil { - if let data = NSData(contentsOfFile: path!) { + if let data = try? Data(contentsOf: URL(fileURLWithPath: path!)) { return data.toJavaBytes() } } - path = NSBundle.framework.pathForResource(name, ofType: nil) + path = Bundle.framework.path(forResource: name, ofType: nil) if path != nil { - if let data = NSData(contentsOfFile: path!) { + if let data = try? Data(contentsOf: URL(fileURLWithPath: path!)) { return data.toJavaBytes() } } return nil } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaCrypto.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaCrypto.swift index 71c6adf5bf..c1bf874299 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaCrypto.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaCrypto.swift @@ -11,14 +11,14 @@ class CocoaCrypto: NSObject, ARCocoaCryptoProxyProvider { return SHA256Digest() } - func createAES128WithKey(key: IOSByteArray!) -> ARBlockCipher! { + func createAES128(withKey key: IOSByteArray!) -> ARBlockCipher! { return AES128(key: key) } } class SHA256Digest: NSObject, ARDigest { - let context = UnsafeMutablePointer.alloc(1) + let context = UnsafeMutablePointer.allocate(capacity: 1) override init() { super.init() @@ -26,39 +26,40 @@ class SHA256Digest: NSObject, ARDigest { } deinit { - context.dealloc(1) + context.deallocate(capacity: 1) } func reset() { CC_SHA256_Init(context) } - func update(src: IOSByteArray!, withOffset offset: jint, withLength length: jint) { + func update(_ src: IOSByteArray!, withOffset offset: jint, withLength length: jint) { let pointer = src .buffer() - .advancedBy(Int(offset)) + .advanced(by: Int(offset)) CC_SHA256_Update(context, pointer, CC_LONG(length)) } - func doFinal(dest: IOSByteArray!, withOffset destOffset: jint) { + func doFinal(_ dest: IOSByteArray!, withOffset destOffset: jint) { - let pointer = UnsafeMutablePointer(dest.buffer()) - .advancedBy(Int(destOffset)) + let pointer = dest.buffer() + .advanced(by: Int(destOffset)) + .bindMemory(to: UInt8.self, capacity: Int(CC_SHA256_DIGEST_LENGTH)) CC_SHA256_Final(pointer, context) } - func getDigestSize() -> jint { + func getSize() -> jint { return jint(CC_SHA256_DIGEST_LENGTH) } } class AES128: NSObject, ARBlockCipher { - var encryptor = UnsafeMutablePointer.alloc(1) - var decryptor = UnsafeMutablePointer.alloc(1) + var encryptor = UnsafeMutablePointer.allocate(capacity: 1) + var decryptor = UnsafeMutablePointer.allocate(capacity: 1) init(key: IOSByteArray) { CCCryptorCreate( @@ -79,33 +80,33 @@ class AES128: NSObject, ARBlockCipher { } deinit { - encryptor.dealloc(1) - decryptor.dealloc(1) + encryptor.deallocate(capacity: 1) + decryptor.deallocate(capacity: 1) } - func encryptBlock(data: IOSByteArray!, withOffset offset: jint, toDest dest: IOSByteArray!, withOffset destOffset: jint) { + func encryptBlock(_ data: IOSByteArray!, withOffset offset: jint, toDest dest: IOSByteArray!, withOffset destOffset: jint) { let src = data.buffer() - .advancedBy(Int(offset)) + .advanced(by: Int(offset)) let dst = dest.buffer() - .advancedBy(Int(destOffset)) + .advanced(by: Int(destOffset)) var bytesOut: Int = 0 - CCCryptorUpdate(encryptor.memory, src, 16, dst, 32, &bytesOut) + CCCryptorUpdate(encryptor.pointee, src, 16, dst, 32, &bytesOut) } - func decryptBlock(data: IOSByteArray!, withOffset offset: jint, toDest dest: IOSByteArray!, withOffset destOffset: jint) { + func decryptBlock(_ data: IOSByteArray!, withOffset offset: jint, toDest dest: IOSByteArray!, withOffset destOffset: jint) { let src = data.buffer() - .advancedBy(Int(offset)) + .advanced(by: Int(offset)) let dst = dest.buffer() - .advancedBy(Int(destOffset)) + .advanced(by: Int(destOffset)) var bytesOut: Int = 0 - CCCryptorUpdate(decryptor.memory, src, 16, dst, 32, &bytesOut) + CCCryptorUpdate(decryptor.pointee, src, 16, dst, 32, &bytesOut) } func getBlockSize() -> jint { return 16 } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaDispatcher.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaDispatcher.swift index dd25624673..3fd384ab11 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaDispatcher.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaDispatcher.swift @@ -6,7 +6,7 @@ import Foundation class CocoaDispatcher: NSObject, ARCocoaDispatcherProxy { - func dispatchOnBackground(runnable: JavaLangRunnable!, withDelay delay: jlong) -> ARDispatchCancel! { + func dispatch(onBackground runnable: JavaLangRunnable!, withDelay delay: jlong) -> ARDispatchCancel! { dispatchBackgroundDelayed(Double(delay) / 1000) { () -> Void in runnable.run() } @@ -16,7 +16,7 @@ class CocoaDispatcher: NSObject, ARCocoaDispatcherProxy { private class DispatchCancel: NSObject, ARDispatchCancel { - @objc private func cancel() { + @objc fileprivate func cancel() { // Do Nothing } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaFileSystemRuntime.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaFileSystemRuntime.swift index 416b74b402..a535530a83 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaFileSystemRuntime.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaFileSystemRuntime.swift @@ -4,11 +4,11 @@ import Foundation -private let documentsFolder = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)[0].asNS.stringByDeletingLastPathComponent +private let documentsFolder = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0].asNS.deletingLastPathComponent // Public methods for working with files -public class CocoaFiles { - public class func pathFromDescriptor(path: String) -> String { +open class CocoaFiles { + open class func pathFromDescriptor(_ path: String) -> String { return documentsFolder + path } } @@ -17,19 +17,19 @@ public class CocoaFiles { @objc class CocoaFileSystemRuntime : NSObject, ARFileSystemRuntime { - let manager = NSFileManager.defaultManager() + let manager = FileManager.default override init() { super.init() } func createTempFile() -> ARFileSystemReference! { - let fileName = "/tmp/\(NSUUID().UUIDString)" - NSFileManager.defaultManager().createFileAtPath(documentsFolder + fileName, contents: nil, attributes: nil) + let fileName = "/tmp/\(UUID().uuidString)" + FileManager.default.createFile(atPath: documentsFolder + fileName, contents: nil, attributes: nil) return CocoaFile(path: fileName) } - func commitTempFile(sourceFile: ARFileSystemReference!, withFileId fileId: jlong, withFileName fileName: String!) -> ARFileSystemReference! { + func commitTempFile(_ sourceFile: ARFileSystemReference!, withFileId fileId: jlong, withFileName fileName: String!) -> ARFileSystemReference! { // Finding file available name @@ -44,15 +44,15 @@ public class CocoaFiles { // } // } - let srcUrl = NSURL(fileURLWithPath: documentsFolder + sourceFile.getDescriptor()!) - let destUrl = NSURL(fileURLWithPath: documentsFolder + descriptor) + let srcUrl = URL(fileURLWithPath: documentsFolder + sourceFile.getDescriptor()!) + let destUrl = URL(fileURLWithPath: documentsFolder + descriptor) // manager.replaceItemAtURL(srcUrl, withItemAtURL: destUrl, backupItemName: nil, options: 0, resultingItemURL: nil) // Moving file to new place do { - try manager.replaceItemAtURL(destUrl, withItemAtURL: srcUrl, backupItemName: nil, options: NSFileManagerItemReplacementOptions(rawValue: 0), resultingItemURL: nil) + try manager.replaceItem(at: destUrl, withItemAt: srcUrl, backupItemName: nil, options: FileManager.ItemReplacementOptions(rawValue: 0), resultingItemURL: nil) // try manager.moveItemAtPath(documentsFolder + sourceFile.getDescriptor()!, toPath: path) return CocoaFile(path: descriptor) @@ -61,7 +61,7 @@ public class CocoaFiles { } } - func fileFromDescriptor(descriptor: String!) -> ARFileSystemReference! { + func file(fromDescriptor descriptor: String!) -> ARFileSystemReference! { return CocoaFile(path: descriptor) } @@ -85,7 +85,7 @@ class CocoaFile : NSObject, ARFileSystemReference { } func isExist() -> Bool { - return NSFileManager().fileExistsAtPath(realPath); + return FileManager().fileExists(atPath: realPath); } func isInAppMemory() -> jboolean { @@ -98,8 +98,8 @@ class CocoaFile : NSObject, ARFileSystemReference { func getSize() -> jint { do { - let attrs = try NSFileManager().attributesOfItemAtPath(realPath) - return jint(NSDictionary.fileSize(attrs)()) + let attrs = try FileManager().attributesOfItem(atPath: realPath) + return jint((attrs[FileAttributeKey.size] as! NSNumber).int32Value) } catch _ { return 0 } @@ -107,24 +107,24 @@ class CocoaFile : NSObject, ARFileSystemReference { func openRead() -> ARPromise! { - let fileHandle = NSFileHandle(forReadingAtPath: realPath) + let fileHandle = FileHandle(forReadingAtPath: realPath) if (fileHandle == nil) { - return ARPromise.failure(JavaLangRuntimeException(NSString: "Unable to open file")) + return ARPromise.failure(JavaLangRuntimeException(nsString: "Unable to open file")) } return ARPromise.success(CocoaInputFile(fileHandle: fileHandle!)) } - func openWriteWithSize(size: jint) -> ARPromise! { - let fileHandle = NSFileHandle(forWritingAtPath: realPath) + func openWrite(withSize size: jint) -> ARPromise! { + let fileHandle = FileHandle(forWritingAtPath: realPath) if (fileHandle == nil) { - return ARPromise.failure(JavaLangRuntimeException(NSString: "Unable to open file")) + return ARPromise.failure(JavaLangRuntimeException(nsString: "Unable to open file")) } - fileHandle!.seekToFileOffset(UInt64(size)) - fileHandle!.seekToFileOffset(0) + fileHandle!.seek(toFileOffset: UInt64(size)) + fileHandle!.seek(toFileOffset: 0) return ARPromise.success(CocoaOutputFile(fileHandle: fileHandle!)) } @@ -132,19 +132,19 @@ class CocoaFile : NSObject, ARFileSystemReference { class CocoaOutputFile : NSObject, AROutputFile { - let fileHandle: NSFileHandle + let fileHandle: FileHandle - init(fileHandle:NSFileHandle) { + init(fileHandle:FileHandle) { self.fileHandle = fileHandle } - func writeWithOffset(fileOffset: jint, withData data: IOSByteArray!, withDataOffset dataOffset: jint, withLength dataLen: jint) -> Bool { + func write(withOffset fileOffset: jint, withData data: IOSByteArray!, withDataOffset dataOffset: jint, withLength dataLen: jint) -> Bool { - let pointer = data.buffer().advancedBy(Int(dataOffset)) - let srcData = NSData(bytesNoCopy: pointer, length: Int(dataLen), freeWhenDone: false) + let pointer = data.buffer().advanced(by: Int(dataOffset)) + let srcData = Data(bytesNoCopy: UnsafeMutableRawPointer(pointer), count: Int(dataLen), deallocator: .none) - fileHandle.seekToFileOffset(UInt64(fileOffset)) - fileHandle.writeData(srcData) + fileHandle.seek(toFileOffset: UInt64(fileOffset)) + fileHandle.write(srcData) return true; } @@ -158,30 +158,25 @@ class CocoaOutputFile : NSObject, AROutputFile { class CocoaInputFile :NSObject, ARInputFile { - let fileHandle:NSFileHandle + let fileHandle:FileHandle - init(fileHandle:NSFileHandle) { + init(fileHandle:FileHandle) { self.fileHandle = fileHandle } - func readWithOffset(fileOffset: jint, withLength len: jint) -> ARPromise! { + func read(withOffset fileOffset: jint, withLength len: jint) -> ARPromise! { return ARPromise { (resolver) in dispatchBackground { - self.fileHandle.seekToFileOffset(UInt64(fileOffset)) + self.fileHandle.seek(toFileOffset: UInt64(fileOffset)) - let readed: NSData = self.fileHandle.readDataOfLength(Int(len)) + let readed = self.fileHandle.readData(ofLength: Int(len)) let data = IOSByteArray(length: UInt(len)) - var srcBuffer = UnsafeMutablePointer(readed.bytes) - var destBuffer = UnsafeMutablePointer(data.buffer()) - let readCount = min(Int(len), Int(readed.length)) - for _ in 0..(readed.bytes) -// var destBuffer = UnsafeMutablePointer(data.buffer()) -// let len = min(Int(len), Int(readed.length)) -// for _ in offset.. Bool { -// self.fileHandle.closeFile() -// return true -// } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaHttpRuntime.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaHttpRuntime.swift index d76bc56617..ec7ea7f187 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaHttpRuntime.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaHttpRuntime.swift @@ -6,21 +6,21 @@ import Foundation class CocoaHttpRuntime: NSObject, ARHttpRuntime { - let queue:NSOperationQueue = NSOperationQueue() + let queue:OperationQueue = OperationQueue() - func getMethodWithUrl(url: String!, withStartOffset startOffset: jint, withSize size: jint, withTotalSize totalSize: jint) -> ARPromise! { + func getMethodWithUrl(_ url: String!, withStartOffset startOffset: jint, withSize size: jint, withTotalSize totalSize: jint) -> ARPromise! { return ARPromise { (resolver) in let header = "bytes=\(startOffset)-\(min(startOffset + size, totalSize))" - let request = NSMutableURLRequest(URL: NSURL(string: url)!) - request.HTTPShouldHandleCookies = false - request.cachePolicy = NSURLRequestCachePolicy.ReloadIgnoringLocalAndRemoteCacheData + let request = NSMutableURLRequest(url: URL(string: url)!) + request.httpShouldHandleCookies = false + request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalAndRemoteCacheData request.setValue(header, forHTTPHeaderField: "Range") - request.HTTPMethod = "GET" + request.httpMethod = "GET" - NSURLConnection.sendAsynchronousRequest(request, queue: self.queue, completionHandler:{ (response: NSURLResponse?, data: NSData?, error: NSError?) -> Void in - if let respHttp = response as? NSHTTPURLResponse { + NSURLConnection.sendAsynchronousRequest(request as URLRequest, queue: self.queue, completionHandler:{ (response: URLResponse?, data: Data?, error: Error?) -> Void in + if let respHttp = response as? HTTPURLResponse { if (respHttp.statusCode >= 200 && respHttp.statusCode < 300) { resolver.result(ARHTTPResponse(code: jint(respHttp.statusCode), withContent: data!.toJavaBytes())) } else { @@ -33,17 +33,17 @@ class CocoaHttpRuntime: NSObject, ARHttpRuntime { } } - func putMethodWithUrl(url: String!, withContents contents: IOSByteArray!) -> ARPromise! { + func putMethod(withUrl url: String!, withContents contents: IOSByteArray!) -> ARPromise! { return ARPromise { (resolver) in - let request = NSMutableURLRequest(URL: NSURL(string: url)!) - request.HTTPShouldHandleCookies = false - request.cachePolicy = NSURLRequestCachePolicy.ReloadIgnoringLocalAndRemoteCacheData - request.HTTPMethod = "PUT" - request.HTTPBody = contents.toNSData() + let request = NSMutableURLRequest(url: URL(string: url)!) + request.httpShouldHandleCookies = false + request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringLocalAndRemoteCacheData + request.httpMethod = "PUT" + request.httpBody = contents.toNSData() request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type") - NSURLConnection.sendAsynchronousRequest(request, queue: self.queue, completionHandler:{ (response: NSURLResponse?, data: NSData?, error: NSError?) -> Void in - if let respHttp = response as? NSHTTPURLResponse { + NSURLConnection.sendAsynchronousRequest(request as URLRequest, queue: self.queue, completionHandler:{ (response: URLResponse?, data: Data?, error: Error?) -> Void in + if let respHttp = response as? HTTPURLResponse { if (respHttp.statusCode >= 200 && respHttp.statusCode < 300) { resolver.result(ARHTTPResponse(code: jint(respHttp.statusCode), withContent: nil)) } else { @@ -55,4 +55,4 @@ class CocoaHttpRuntime: NSObject, ARHttpRuntime { }) } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaLifecycleRuntime.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaLifecycleRuntime.swift index 8fd5255d9a..11a10e76c3 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaLifecycleRuntime.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaLifecycleRuntime.swift @@ -17,17 +17,17 @@ import Foundation @objc class CocoaWakeLock: NSObject, ARWakeLock { - private var background: UIBackgroundTaskIdentifier? + fileprivate var background: UIBackgroundTaskIdentifier? override init() { - background = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler(nil) + background = UIApplication.shared.beginBackgroundTask(expirationHandler: nil) super.init() } - func releaseLock() { + public func close() { if background != nil { - UIApplication.sharedApplication().endBackgroundTask(background!) + UIApplication.shared.endBackgroundTask(background!) background = nil } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaNetworkRuntime.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaNetworkRuntime.swift index 652580f833..4d9351270e 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaNetworkRuntime.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaNetworkRuntime.swift @@ -3,7 +3,6 @@ // import Foundation -import CocoaAsyncSocket class CocoaNetworkRuntime : ARManagedNetworkProvider { @@ -14,9 +13,9 @@ class CocoaNetworkRuntime : ARManagedNetworkProvider { class CocoaTcpConnectionFactory: NSObject, ARAsyncConnectionFactory { - func createConnectionWithConnectionId(connectionId: jint, - withEndpoint endpoint: ARConnectionEndpoint!, - withInterface connectionInterface: ARAsyncConnectionInterface!) -> ARAsyncConnection! { + func createConnection(withConnectionId connectionId: jint, + with endpoint: ARConnectionEndpoint!, + with connectionInterface: ARAsyncConnectionInterface!) -> ARAsyncConnection! { return CocoaTcpConnection(connectionId: Int(connectionId), endpoint: endpoint, connection: connectionInterface) } @@ -24,7 +23,7 @@ class CocoaTcpConnectionFactory: NSObject, ARAsyncConnectionFactory { class CocoaTcpConnection: ARAsyncConnection, GCDAsyncSocketDelegate { - static let queue = dispatch_queue_create("im.actor.network", DISPATCH_QUEUE_SERIAL) + static let queue = DispatchQueue(label: "im.actor.network", attributes: []) let READ_HEADER = 1 let READ_BODY = 2 @@ -32,10 +31,10 @@ class CocoaTcpConnection: ARAsyncConnection, GCDAsyncSocketDelegate { var TAG: String! var gcdSocket:GCDAsyncSocket? = nil - var header: NSData? + var header: Data? init(connectionId: Int, endpoint: ARConnectionEndpoint!, connection: ARAsyncConnectionInterface!) { - super.init(endpoint: endpoint, withInterface: connection) + super.init(endpoint: endpoint, with: connection) TAG = "🎍ConnectionTcp#\(connectionId)" } @@ -43,17 +42,17 @@ class CocoaTcpConnection: ARAsyncConnection, GCDAsyncSocketDelegate { let endpoint = getEndpoint() gcdSocket = GCDAsyncSocket(delegate: self, delegateQueue: CocoaTcpConnection.queue) - gcdSocket?.IPv4PreferredOverIPv6 = false + gcdSocket?.isIPv4PreferredOverIPv6 = false do { - try self.gcdSocket!.connectToHost(endpoint.host!, onPort: UInt16(endpoint.port), withTimeout: Double(ARManagedConnection_CONNECTION_TIMEOUT) / 1000.0) + try self.gcdSocket!.connect(toHost: (endpoint?.host!)!, onPort: UInt16((endpoint?.port)!), withTimeout: Double(ARManagedConnection_CONNECTION_TIMEOUT) / 1000.0) } catch _ { } } // Affer successful connection - func socket(sock: GCDAsyncSocket!, didConnectToHost host: String!, port: UInt16) { - if (self.getEndpoint().type == ARConnectionEndpoint.TYPE_TCP_TLS()) { + func socket(_ sock: GCDAsyncSocket!, didConnectToHost host: String!, port: UInt16) { + if (self.getEndpoint().type == ARConnectionEndpoint.type_TCP_TLS()) { // NSLog("\(TAG) Starring TLS Session...") sock.startTLS(nil) } else { @@ -62,39 +61,39 @@ class CocoaTcpConnection: ARAsyncConnection, GCDAsyncSocketDelegate { } // After TLS successful - func socketDidSecure(sock: GCDAsyncSocket!) { + func socketDidSecure(_ sock: GCDAsyncSocket!) { // NSLog("\(TAG) TLS Session started...") startConnection() } func startConnection() { - gcdSocket?.readDataToLength(UInt(9), withTimeout: -1, tag: READ_HEADER) + gcdSocket?.readData(toLength: UInt(9), withTimeout: -1, tag: READ_HEADER) onConnected() } // On connection closed - func socketDidDisconnect(sock: GCDAsyncSocket!, withError err: NSError!) { + func socketDidDisconnect(_ sock: GCDAsyncSocket!, withError err: Error?) { // NSLog("\(TAG) Connection closed...") onClosed() } - func socket(sock: GCDAsyncSocket!, didReadData data: NSData!, withTag tag: Int) { + func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) { if (tag == READ_HEADER) { // NSLog("\(TAG) Header received") self.header = data data.readUInt32(0) // IGNORE: package id let size = data.readUInt32(5) - gcdSocket?.readDataToLength(UInt(size + 4), withTimeout: -1, tag: READ_BODY) + gcdSocket?.readData(toLength: UInt(size + 4), withTimeout: -1, tag: READ_BODY) } else if (tag == READ_BODY) { // NSLog("\(TAG) Body received") - let package = NSMutableData() - package.appendData(self.header!) - package.appendData(data) + var package = Data() + package.append(self.header!) + package.append(data) package.readUInt32(0) // IGNORE: package id self.header = nil onReceived(package.toJavaBytes()) - gcdSocket?.readDataToLength(UInt(9), withTimeout: -1, tag: READ_HEADER) + gcdSocket?.readData(toLength: UInt(9), withTimeout: -1, tag: READ_HEADER) } else { fatalError("Unknown tag in read data") } @@ -108,22 +107,22 @@ class CocoaTcpConnection: ARAsyncConnection, GCDAsyncSocketDelegate { } } - override func doSend(data: IOSByteArray!) { - gcdSocket?.writeData(data.toNSData(), withTimeout: -1, tag: 0) + override func doSend(_ data: IOSByteArray!) { + gcdSocket?.write(data.toNSData(), withTimeout: -1, tag: 0) } } -private extension NSData { +private extension Data { func readUInt32() -> UInt32 { var raw: UInt32 = 0; - self.getBytes(&raw, length: 4) + (self as NSData).getBytes(&raw, length: 4) return raw.bigEndian } - func readUInt32(offset: Int) -> UInt32 { + func readUInt32(_ offset: Int) -> UInt32 { var raw: UInt32 = 0; - self.getBytes(&raw, range: NSMakeRange(offset, 4)) + (self as NSData).getBytes(&raw, range: NSMakeRange(offset, 4)) return raw.bigEndian } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaStorageRuntime.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaStorageRuntime.swift index 18c18f7bbc..1f71580d0d 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaStorageRuntime.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaStorageRuntime.swift @@ -10,19 +10,19 @@ import Foundation let preferences = UDPreferencesStorage() override init() { - self.dbPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, - .UserDomainMask, true)[0].asNS.stringByAppendingPathComponent("actor.db") + self.dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, + .userDomainMask, true)[0].asNS.appendingPathComponent("actor.db") } func createPreferencesStorage() -> ARPreferencesStorage! { return preferences } - func createKeyValueWithName(name: String!) -> ARKeyValueStorage! { + func createKeyValue(withName name: String!) -> ARKeyValueStorage! { return FMDBKeyValue(databasePath: dbPath, tableName: name) } - func createListWithName(name: String!) -> ARListStorage! { + func createList(withName name: String!) -> ARListStorage! { return FMDBList(databasePath: dbPath, tableName: name) } @@ -30,8 +30,8 @@ import Foundation preferences.clear() let db = FMDatabase(path: dbPath) - db.open() - db.executeStatements("select 'drop table ' || name || ';' from sqlite_master where type = 'table';") - db.close() + db?.open() + db?.executeStatements("select 'drop table ' || name || ';' from sqlite_master where type = 'table';") + db?.close() } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaWebRTCRuntime.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaWebRTCRuntime.swift index fb2b21c19e..cbaece30f5 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaWebRTCRuntime.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/CocoaWebRTCRuntime.swift @@ -5,39 +5,39 @@ import Foundation import AVFoundation -let queue = dispatch_queue_create("My Queue", DISPATCH_QUEUE_SERIAL); +let queue = DispatchQueue(label: "My Queue", attributes: []); class CocoaWebRTCRuntime: NSObject, ARWebRTCRuntime { - private var isInited: Bool = false - private var peerConnectionFactory: RTCPeerConnectionFactory! - private var videoSource: RTCVideoSource! - private var videoSourceLoaded = false + fileprivate var isInited: Bool = false + fileprivate var peerConnectionFactory: RTCPeerConnectionFactory! + fileprivate var videoSource: RTCVideoSource! + fileprivate var videoSourceLoaded = false override init() { } - func getUserMediaWithIsAudioEnabled(isAudioEnabled: jboolean, withIsVideoEnabled isVideoEnabled: jboolean) -> ARPromise { + func getUserMedia(withIsAudioEnabled isAudioEnabled: jboolean, withIsVideoEnabled isVideoEnabled: jboolean) -> ARPromise { return ARPromise { (resolver) -> () in - dispatch_async(queue) { + queue.async { self.initRTC() // Unfortinatelly building capture source "on demand" causes some weird internal crashes self.initVideo() - let stream = self.peerConnectionFactory.mediaStreamWithLabel("ARDAMSv0") + let stream = self.peerConnectionFactory.mediaStream(withLabel: "ARDAMSv0") // // Audio // if isAudioEnabled { - let audio = self.peerConnectionFactory.audioTrackWithID("audio0") - stream.addAudioTrack(audio) + let audio = self.peerConnectionFactory.audioTrack(withID: "audio0") + stream?.addAudioTrack(audio) } // @@ -45,20 +45,20 @@ class CocoaWebRTCRuntime: NSObject, ARWebRTCRuntime { // if isVideoEnabled { if self.videoSource != nil { - let localVideoTrack = self.peerConnectionFactory.videoTrackWithID("video0", source: self.videoSource) - stream.addVideoTrack(localVideoTrack) + let localVideoTrack = self.peerConnectionFactory.videoTrack(withID: "video0", source: self.videoSource) + stream?.addVideoTrack(localVideoTrack) } } - resolver.result(MediaStream(stream:stream)) + resolver.result(MediaStream(stream:stream!)) } } } - func createPeerConnectionWithServers(webRTCIceServers: IOSObjectArray!, withSettings settings: ARWebRTCSettings!) -> ARPromise { + func createPeerConnection(withServers webRTCIceServers: IOSObjectArray!, with settings: ARWebRTCSettings!) -> ARPromise { let servers: [ARWebRTCIceServer] = webRTCIceServers.toSwiftArray() return ARPromise { (resolver) -> () in - dispatch_async(queue) { + queue.async { self.initRTC() resolver.result(CocoaWebRTCPeerConnection(servers: servers, peerConnectionFactory: self.peerConnectionFactory)) } @@ -78,15 +78,15 @@ class CocoaWebRTCRuntime: NSObject, ARWebRTCRuntime { self.videoSourceLoaded = true var cameraID: String? - for captureDevice in AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo) { - if captureDevice.position == AVCaptureDevicePosition.Front { - cameraID = captureDevice.localizedName + for captureDevice in AVCaptureDevice.devices(withMediaType: AVMediaTypeVideo) { + if (captureDevice as AnyObject).position == AVCaptureDevicePosition.front { + cameraID = (captureDevice as AnyObject).localizedName } } if(cameraID != nil) { let videoCapturer = RTCVideoCapturer(deviceName: cameraID) - self.videoSource = self.peerConnectionFactory.videoSourceWithCapturer(videoCapturer, constraints: RTCMediaConstraints()) + self.videoSource = self.peerConnectionFactory.videoSource(with: videoCapturer, constraints: RTCMediaConstraints()) } } } @@ -112,13 +112,13 @@ class CocoaWebRTCRuntime: NSObject, ARWebRTCRuntime { for i in 0.. jint { + open func getType() -> jint { return ARWebRTCTrackType_AUDIO } - public func setEnabledWithBoolean(isEnabled: jboolean) { + open func setEnabledWithBoolean(_ isEnabled: jboolean) { audioTrack.setEnabled(isEnabled) } - public func isEnabled() -> jboolean { + open func isEnabled() -> jboolean { return audioTrack.isEnabled() } } -public class CocoaVideoTrack: NSObject, ARWebRTCMediaTrack { +open class CocoaVideoTrack: NSObject, ARWebRTCMediaTrack { - public let videoTrack: RTCVideoTrack + open let videoTrack: RTCVideoTrack - public init(let videoTrack: RTCVideoTrack) { + public init(videoTrack: RTCVideoTrack) { self.videoTrack = videoTrack } - public func getTrackType() -> jint { + open func getType() -> jint { return ARWebRTCTrackType_VIDEO } - public func setEnabledWithBoolean(isEnabled: jboolean) { + open func setEnabledWithBoolean(_ isEnabled: jboolean) { videoTrack.setEnabled(isEnabled) } - public func isEnabled() -> jboolean { + open func isEnabled() -> jboolean { return videoTrack.isEnabled() } } class CocoaWebRTCPeerConnection: NSObject, ARWebRTCPeerConnection, RTCPeerConnectionDelegate { - private var peerConnection: RTCPeerConnection! - private var callbacks = [ARWebRTCPeerConnectionCallback]() - private let peerConnectionFactory: RTCPeerConnectionFactory - private var ownStreams = [ARCountedReference]() + fileprivate var peerConnection: RTCPeerConnection! + fileprivate var callbacks = [ARWebRTCPeerConnectionCallback]() + fileprivate let peerConnectionFactory: RTCPeerConnectionFactory + fileprivate var ownStreams = [ARCountedReference]() init(servers: [ARWebRTCIceServer], peerConnectionFactory: RTCPeerConnectionFactory) { self.peerConnectionFactory = peerConnectionFactory @@ -201,44 +201,44 @@ class CocoaWebRTCPeerConnection: NSObject, ARWebRTCPeerConnection, RTCPeerConnec let iceServers = servers.map { (src) -> RTCICEServer in if (src.username == nil || src.credential == nil) { - return RTCICEServer(URI: NSURL(string: src.url), username: "", password: "") + return RTCICEServer(uri: URL(string: src.url), username: "", password: "") } else { - return RTCICEServer(URI: NSURL(string: src.url), username: src.username, password: src.credential) + return RTCICEServer(uri: URL(string: src.url), username: src.username, password: src.credential) } } - peerConnection = peerConnectionFactory.peerConnectionWithICEServers(iceServers, constraints: RTCMediaConstraints(), delegate: self) + peerConnection = peerConnectionFactory.peerConnection(withICEServers: iceServers, constraints: RTCMediaConstraints(), delegate: self) AAAudioManager.sharedAudio().peerConnectionStarted() } - func addCallback(callback: ARWebRTCPeerConnectionCallback) { + func add(_ callback: ARWebRTCPeerConnectionCallback) { - if !callbacks.contains({ callback.isEqual($0) }) { + if !callbacks.contains(where: { callback.isEqual($0) }) { callbacks.append(callback) } } - func removeCallback(callback: ARWebRTCPeerConnectionCallback) { - let index = callbacks.indexOf({ callback.isEqual($0) }) + func remove(_ callback: ARWebRTCPeerConnectionCallback) { + let index = callbacks.index(where: { callback.isEqual($0) }) if index != nil { - callbacks.removeAtIndex(index!) + callbacks.remove(at: index!) } } - func addCandidateWithIndex(index: jint, withId id_: String, withSDP sdp: String) { - peerConnection.addICECandidate(RTCICECandidate(mid: id_, index: Int(index), sdp: sdp)) + func addCandidate(with index: jint, withId id_: String, withSDP sdp: String) { + peerConnection.add(RTCICECandidate(mid: id_, index: Int(index), sdp: sdp)) } - func addOwnStream(stream: ARCountedReference) { + func addOwnStream(_ stream: ARCountedReference) { stream.acquire() let ms = (stream.get() as! MediaStream) ownStreams.append(stream) - peerConnection.addStream(ms.stream) + peerConnection.add(ms.stream) } - func removeOwnStream(stream: ARCountedReference) { + func removeOwnStream(_ stream: ARCountedReference) { if ownStreams.contains(stream) { let ms = (stream.get() as! MediaStream) - peerConnection.removeStream(ms.stream) + peerConnection.remove(ms.stream) stream.release__() } } @@ -247,11 +247,11 @@ class CocoaWebRTCPeerConnection: NSObject, ARWebRTCPeerConnection, RTCPeerConnec return ARPromise(closure: { (resolver) -> () in let constraints = RTCMediaConstraints(mandatoryConstraints: [RTCPair(key: "OfferToReceiveAudio", value: "true"), RTCPair(key: "OfferToReceiveVideo", value: "true")], optionalConstraints: []) - self.peerConnection.createAnswer(constraints, didCreate: { (desc, error) -> () in + self.peerConnection.createAnswer(constraints!, didCreate: { (desc, error) -> () in if error == nil { - resolver.result(ARWebRTCSessionDescription(type: "answer", withSDP: desc.description)) + resolver.result(ARWebRTCSessionDescription(type: "answer", withSDP: desc!.description)) } else { - resolver.error(JavaLangException(NSString: "Error \(error.description)")) + resolver.error(JavaLangException(nsString: "Error \(error!)")) } }) }) @@ -261,35 +261,35 @@ class CocoaWebRTCPeerConnection: NSObject, ARWebRTCPeerConnection, RTCPeerConnec return ARPromise(closure: { (resolver) -> () in let constraints = RTCMediaConstraints(mandatoryConstraints: [RTCPair(key: "OfferToReceiveAudio", value: "true"), RTCPair(key: "OfferToReceiveVideo", value: "true")], optionalConstraints: []) - self.peerConnection.createOffer(constraints, didCreate: { (desc, error) -> () in + self.peerConnection.createOffer(constraints!, didCreate: { (desc, error) -> () in if error == nil { - resolver.result(ARWebRTCSessionDescription(type: "offer", withSDP: desc.description)) + resolver.result(ARWebRTCSessionDescription(type: "offer", withSDP: desc!.description)) } else { - resolver.error(JavaLangException(NSString: "Error \(error.description)")) + resolver.error(JavaLangException(nsString: "Error \(error!)")) } }) }) } - func setRemoteDescription(description_: ARWebRTCSessionDescription) -> ARPromise { + func setRemoteDescription(_ description_: ARWebRTCSessionDescription) -> ARPromise { return ARPromise(executor: AAPromiseFunc(closure: { (resolver) -> () in self.peerConnection.setRemoteDescription(RTCSessionDescription(type: description_.type, sdp: description_.sdp), didSet: { (error) -> () in if (error == nil) { resolver.result(description_) } else { - resolver.error(JavaLangException(NSString: "Error \(error.description)")) + resolver.error(JavaLangException(nsString: "Error \(error)")) } }) })) } - func setLocalDescription(description_: ARWebRTCSessionDescription) -> ARPromise { + func setLocalDescription(_ description_: ARWebRTCSessionDescription) -> ARPromise { return ARPromise(executor: AAPromiseFunc(closure: { (resolver) -> () in self.peerConnection.setLocalDescription(RTCSessionDescription(type: description_.type, sdp: description_.sdp), didSet: { (error) -> () in if (error == nil) { resolver.result(description_) } else { - resolver.error(JavaLangException(NSString: "Error \(error.description)")) + resolver.error(JavaLangException(nsString: "Error \(error)")) } }) })) @@ -299,7 +299,7 @@ class CocoaWebRTCPeerConnection: NSObject, ARWebRTCPeerConnection, RTCPeerConnec func close() { for s in ownStreams { let ms = s.get() as! MediaStream - peerConnection.removeStream(ms.stream) + peerConnection.remove(ms.stream) s.release__() } ownStreams.removeAll() @@ -312,43 +312,43 @@ class CocoaWebRTCPeerConnection: NSObject, ARWebRTCPeerConnection, RTCPeerConnec // - func peerConnection(peerConnection: RTCPeerConnection!, addedStream stream: RTCMediaStream!) { + func peerConnection(_ peerConnection: RTCPeerConnection!, addedStream stream: RTCMediaStream!) { for c in callbacks { c.onStreamAdded(MediaStream(stream: stream!)) } } - func peerConnection(peerConnection: RTCPeerConnection!, removedStream stream: RTCMediaStream!) { + func peerConnection(_ peerConnection: RTCPeerConnection!, removedStream stream: RTCMediaStream!) { for c in callbacks { c.onStreamRemoved(MediaStream(stream: stream!)) } } - func peerConnectionOnRenegotiationNeeded(peerConnection: RTCPeerConnection!) { + func peerConnection(onRenegotiationNeeded peerConnection: RTCPeerConnection!) { for c in callbacks { c.onRenegotiationNeeded() } } - func peerConnection(peerConnection: RTCPeerConnection!, gotICECandidate candidate: RTCICECandidate!) { + func peerConnection(_ peerConnection: RTCPeerConnection!, gotICECandidate candidate: RTCICECandidate!) { for c in callbacks { - c.onCandidateWithLabel(jint(candidate.sdpMLineIndex), withId: candidate.sdpMid, withCandidate: candidate.sdp) + c.onCandidate(withLabel: jint(candidate.sdpMLineIndex), withId: candidate.sdpMid, withCandidate: candidate.sdp) } } - func peerConnection(peerConnection: RTCPeerConnection!, signalingStateChanged stateChanged: RTCSignalingState) { + func peerConnection(_ peerConnection: RTCPeerConnection!, signalingStateChanged stateChanged: RTCSignalingState) { } - func peerConnection(peerConnection: RTCPeerConnection!, iceConnectionChanged newState: RTCICEConnectionState) { + func peerConnection(_ peerConnection: RTCPeerConnection!, iceConnectionChanged newState: RTCICEConnectionState) { } - func peerConnection(peerConnection: RTCPeerConnection!, iceGatheringChanged newState: RTCICEGatheringState) { + func peerConnection(_ peerConnection: RTCPeerConnection!, iceGatheringChanged newState: RTCICEGatheringState) { } - func peerConnection(peerConnection: RTCPeerConnection!, didOpenDataChannel dataChannel: RTCDataChannel!) { + func peerConnection(_ peerConnection: RTCPeerConnection!, didOpen dataChannel: RTCDataChannel!) { } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/FMDBBridge.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/FMDBBridge.swift index da1ad96917..1a9bcd574c 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/FMDBBridge.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/FMDBBridge.swift @@ -9,32 +9,31 @@ extension ARListEngineRecord { if (self.getQuery() == nil) { return NSNull() } else { - return self.getQuery()!.lowercaseString + return self.getQuery()!.lowercased() as AnyObject } } } extension jlong { func toNSNumber() -> NSNumber { - return NSNumber(longLong: self) + return NSNumber(value: self as Int64) } } extension jint { func toNSNumber() -> NSNumber { - return NSNumber(int: self) + return NSNumber(value: self as Int32) } } extension JavaLangLong { func toNSNumber() -> NSNumber { - return NSNumber(longLong: self.longLongValue()) + return NSNumber(value: self.longLongValue() as Int64) } } -extension NSData { - - func readNSData(offset: Int, len: Int) -> NSData { - return self.subdataWithRange(NSMakeRange(Int(offset), Int(len))) - } -} \ No newline at end of file +extension Data { + func readNSData(_ offset: Int, len: Int) -> Data { + return self.subdata(in: self.startIndex.advanced(by: Int(offset)).. FMResultSet? { - return executeQuery(sql, withArgumentsInArray: values); + func executeQuery(_ sql:String, _ values: Any...) -> FMResultSet? { + return executeQuery(sql, withArgumentsIn: values); } /// This is a rendition of executeUpdate that handles Swift variadic parameters @@ -27,8 +27,8 @@ extension FMDatabase { /// /// :returns: This returns true if successful. Returns false upon error. - func executeUpdate(sql:String, _ values: AnyObject...) -> Bool { - return executeUpdate(sql, withArgumentsInArray: values); + func executeUpdate(_ sql:String, _ values: Any...) -> Bool { + return executeUpdate(sql, withArgumentsIn: values); } /// Private generic function used for the variadic renditions of the FMDatabaseAdditions methods @@ -39,12 +39,12 @@ extension FMDatabase { /// /// :returns: This returns the T value if value is found. Returns nil if column is NULL or upon error. - private func valueForQuery(sql: String, values: NSArray?, completionHandler:(FMResultSet)->(T!)) -> T! { + fileprivate func valueForQuery(_ sql: String, values: Array?, completionHandler:(FMResultSet)->(T!)) -> T! { var result: T! - if let rs = executeQuery(sql, withArgumentsInArray: values! as [AnyObject]) { + if let rs = executeQuery(sql, withArgumentsIn: values!) { if rs.next() { - let obj = rs.objectForColumnIndex(0) as! NSObject + let obj = rs.object(forColumnIndex: 0) as! NSObject if !(obj is NSNull) { result = completionHandler(rs) } @@ -63,8 +63,8 @@ extension FMDatabase { /// /// :returns: This returns string value if value is found. Returns nil if column is NULL or upon error. - func stringForQuery(sql: String, _ values: AnyObject...) -> String! { - return valueForQuery(sql, values: values as NSArray) { $0.stringForColumnIndex(0) } + func stringForQuery(_ sql: String, _ values: Any...) -> String! { + return valueForQuery(sql, values: values) { $0.string(forColumnIndex: 0) } } /// This is a rendition of intForQuery that handles Swift variadic parameters @@ -75,8 +75,8 @@ extension FMDatabase { /// /// :returns: This returns integer value if value is found. Returns nil if column is NULL or upon error. - func intForQuery(sql: String, _ values: AnyObject...) -> Int32! { - return valueForQuery(sql, values: values as NSArray) { $0.intForColumnIndex(0) } + func intForQuery(_ sql: String, _ values: Any...) -> Int32! { + return valueForQuery(sql, values: values) { $0.int(forColumnIndex: 0) } } /// This is a rendition of longForQuery that handles Swift variadic parameters @@ -87,8 +87,8 @@ extension FMDatabase { /// /// :returns: This returns long value if value is found. Returns nil if column is NULL or upon error. - func longForQuery(sql: String, _ values: AnyObject...) -> Int! { - return valueForQuery(sql, values: values as NSArray) { $0.longForColumnIndex(0) } + func longForQuery(_ sql: String, _ values: Any...) -> Int! { + return valueForQuery(sql, values: values) { $0.long(forColumnIndex: 0) } } /// This is a rendition of boolForQuery that handles Swift variadic parameters @@ -99,8 +99,8 @@ extension FMDatabase { /// /// :returns: This returns Bool value if value is found. Returns nil if column is NULL or upon error. - func boolForQuery(sql: String, _ values: AnyObject...) -> Bool! { - return valueForQuery(sql, values: values as NSArray) { $0.boolForColumnIndex(0) } + func boolForQuery(_ sql: String, _ values: Any...) -> Bool! { + return valueForQuery(sql, values: values) { $0.bool(forColumnIndex: 0) } } /// This is a rendition of doubleForQuery that handles Swift variadic parameters @@ -111,8 +111,8 @@ extension FMDatabase { /// /// :returns: This returns Double value if value is found. Returns nil if column is NULL or upon error. - func doubleForQuery(sql: String, _ values: AnyObject...) -> Double! { - return valueForQuery(sql, values: values as NSArray) { $0.doubleForColumnIndex(0) } + func doubleForQuery(_ sql: String, _ values: Any...) -> Double! { + return valueForQuery(sql, values: values) { $0.double(forColumnIndex: 0) } } /// This is a rendition of dateForQuery that handles Swift variadic parameters @@ -123,8 +123,8 @@ extension FMDatabase { /// /// :returns: This returns NSDate value if value is found. Returns nil if column is NULL or upon error. - func dateForQuery(sql: String, _ values: AnyObject...) -> NSDate! { - return valueForQuery(sql, values: values as NSArray) { $0.dateForColumnIndex(0) } + func dateForQuery(_ sql: String, _ values: Any...) -> Date! { + return valueForQuery(sql, values: values) { $0.date(forColumnIndex: 0) } } /// This is a rendition of dataForQuery that handles Swift variadic parameters @@ -135,7 +135,7 @@ extension FMDatabase { /// /// :returns: This returns NSData value if value is found. Returns nil if column is NULL or upon error. - func dataForQuery(sql: String, _ values: AnyObject...) -> NSData! { - return valueForQuery(sql, values: values as NSArray) { $0.dataForColumnIndex(0) } + func dataForQuery(_ sql: String, _ values: Any...) -> Data! { + return valueForQuery(sql, values: values) { $0.data(forColumnIndex: 0) } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/FMDBKeyValue.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/FMDBKeyValue.swift index 982e1ab713..6fdd82d4fe 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/FMDBKeyValue.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/FMDBKeyValue.swift @@ -40,7 +40,7 @@ import Foundation super.init() } - private func checkTable() { + fileprivate func checkTable() { if (isTableChecked) { return } @@ -53,37 +53,37 @@ import Foundation } } - func addOrUpdateItems(values: JavaUtilList!) { + func addOrUpdateItems(_ values: JavaUtilList!) { checkTable() db.beginTransaction() for i in 0.. IOSByteArray! { + func loadItem(withKey key: jlong) -> IOSByteArray! { checkTable() let result = db.dataForQuery(queryItem, key.toNSNumber()) if (result == nil) { return nil } - return result.toJavaBytes() + return result!.toJavaBytes() } func loadAllItems() -> JavaUtilList! { checkTable() - let res = JavaUtilArrayList() + let res = JavaUtilArrayList()! if let result = db.executeQuery(queryAll) { while(result.next()) { - res.addWithId(ARKeyValueRecord(key: jlong(result.longLongIntForColumn("ID")), withData: result.dataForColumn("BYTES").toJavaBytes())) + res.add(withId: ARKeyValueRecord(key: jlong(result.longLongInt(forColumn: "ID")), withData: result.data(forColumn: "BYTES").toJavaBytes())) } } return res } - func loadItems(keys: IOSLongArray!) -> JavaUtilList! { + func loadItems(_ keys: IOSLongArray!) -> JavaUtilList! { checkTable() // Converting to NSNumbers var ids = [NSNumber]() for i in 0.. 0 } else { @@ -178,7 +178,7 @@ class FMDBList : NSObject, ARListStorageDisplayEx { db!.commit() } - func loadItemWithKey(key: jlong) -> ARListEngineRecord! { + func loadItem(withKey key: jlong) -> ARListEngineRecord! { checkTable(); let result = db!.executeQuery(queryItem, key.toNSNumber()); @@ -186,11 +186,11 @@ class FMDBList : NSObject, ARListStorageDisplayEx { return nil } if (result!.next()) { - var query: AnyObject! = result!.objectForColumnName("QUERY"); + var query: AnyObject! = result!.object(forColumnName: "QUERY") as AnyObject!; if (query is NSNull){ query = nil } - let res = ARListEngineRecord(key: jlong(result!.longLongIntForColumn("ID")), withOrder: jlong(result!.longLongIntForColumn("SORT_KEY")), withQuery: query as! String?, withData: result!.dataForColumn("BYTES").toJavaBytes()) + let res = ARListEngineRecord(key: jlong(result!.longLongInt(forColumn: "ID")), withOrder: jlong(result!.longLongInt(forColumn: "SORT_KEY")), withQuery: query as! String?, withData: result!.data(forColumn: "BYTES").toJavaBytes()) result?.close() return res; } else { @@ -205,7 +205,7 @@ class FMDBList : NSObject, ARListStorageDisplayEx { return res } - func loadForwardWithSortKey(sortingKey: JavaLangLong!, withLimit limit: jint) -> JavaUtilList! { + func loadForward(withSortKey sortingKey: JavaLangLong!, withLimit limit: jint) -> JavaUtilList! { checkTable(); var result : FMResultSet? = nil; if (sortingKey == nil) { @@ -221,33 +221,33 @@ class FMDBList : NSObject, ARListStorageDisplayEx { let res: JavaUtilArrayList = JavaUtilArrayList(); - let queryIndex = result!.columnIndexForName("QUERY") - let idIndex = result!.columnIndexForName("ID") - let sortKeyIndex = result!.columnIndexForName("SORT_KEY") - let bytesIndex = result!.columnIndexForName("BYTES") + let queryIndex = result!.columnIndex(forName: "QUERY") + let idIndex = result!.columnIndex(forName: "ID") + let sortKeyIndex = result!.columnIndex(forName: "SORT_KEY") + let bytesIndex = result!.columnIndex(forName: "BYTES") var dataSize = 0 var rowCount = 0 while(result!.next()) { - let key = jlong(result!.longLongIntForColumnIndex(idIndex)) - let order = jlong(result!.longLongIntForColumnIndex(sortKeyIndex)) - var query: AnyObject! = result!.objectForColumnIndex(queryIndex) + let key = jlong(result!.longLongInt(forColumnIndex: idIndex)) + let order = jlong(result!.longLongInt(forColumnIndex: sortKeyIndex)) + var query: AnyObject! = result!.object(forColumnIndex: queryIndex) as AnyObject! if (query is NSNull) { query = nil } - let data = result!.dataForColumnIndex(bytesIndex).toJavaBytes() + let data = result!.data(forColumnIndex: bytesIndex).toJavaBytes() dataSize += Int(data.length()) rowCount += 1 let record = ARListEngineRecord(key: key, withOrder: order, withQuery: query as! String?, withData: data) - res.addWithId(record) + res.add(withId: record) } result!.close() return res; } - func loadForwardWithQuery(query: String!, withSortKey sortingKey: JavaLangLong!, withLimit limit: jint) -> JavaUtilList! { + func loadForward(withQuery query: String!, withSortKey sortingKey: JavaLangLong!, withLimit limit: jint) -> JavaUtilList! { checkTable(); var result : FMResultSet? = nil; @@ -264,12 +264,12 @@ class FMDBList : NSObject, ARListStorageDisplayEx { let res: JavaUtilArrayList = JavaUtilArrayList(); while(result!.next()) { - var query: AnyObject! = result!.objectForColumnName("QUERY"); + var query: AnyObject! = result!.object(forColumnName: "QUERY") as AnyObject!; if (query is NSNull) { query = nil } - let record = ARListEngineRecord(key: jlong(result!.longLongIntForColumn("ID")), withOrder: jlong(result!.longLongIntForColumn("SORT_KEY")), withQuery: query as! String?, withData: result!.dataForColumn("BYTES").toJavaBytes()) - res.addWithId(record) + let record = ARListEngineRecord(key: jlong(result!.longLongInt(forColumn: "ID")), withOrder: jlong(result!.longLongInt(forColumn: "SORT_KEY")), withQuery: query as! String?, withData: result!.data(forColumn: "BYTES").toJavaBytes()) + res.add(withId: record) } result!.close() @@ -277,7 +277,7 @@ class FMDBList : NSObject, ARListStorageDisplayEx { } - func loadBackwardWithSortKey(sortingKey: JavaLangLong!, withLimit limit: jint) -> JavaUtilList! { + func loadBackward(withSortKey sortingKey: JavaLangLong!, withLimit limit: jint) -> JavaUtilList! { checkTable(); var result : FMResultSet? = nil; if (sortingKey == nil) { @@ -293,25 +293,25 @@ class FMDBList : NSObject, ARListStorageDisplayEx { let res: JavaUtilArrayList = JavaUtilArrayList(); while(result!.next()) { - var query: AnyObject! = result!.objectForColumnName("QUERY"); + var query: AnyObject! = result!.object(forColumnName: "QUERY") as AnyObject!; if (query is NSNull) { query = nil } - let record = ARListEngineRecord(key: jlong(result!.longLongIntForColumn("ID")), withOrder: jlong(result!.longLongIntForColumn("SORT_KEY")), withQuery: query as! String?, withData: result!.dataForColumn("BYTES").toJavaBytes()) - res.addWithId(record) + let record = ARListEngineRecord(key: jlong(result!.longLongInt(forColumn: "ID")), withOrder: jlong(result!.longLongInt(forColumn: "SORT_KEY")), withQuery: query as! String?, withData: result!.data(forColumn: "BYTES").toJavaBytes()) + res.add(withId: record) } result!.close() return res; } - func loadBackwardWithQuery(query: String!, withSortKey sortingKey: JavaLangLong!, withLimit limit: jint) -> JavaUtilList! { + func loadBackward(withQuery query: String!, withSortKey sortingKey: JavaLangLong!, withLimit limit: jint) -> JavaUtilList! { checkTable(); var result : FMResultSet? = nil; if (sortingKey == nil) { - result = db!.executeQuery(queryBackwardFilterFirst, query + "%", "% " + query + "%", limit.toNSNumber()); + result = db!.executeQuery(queryBackwardFilterFirst, query + "%", "% " + query + "%", limit.toNSNumber()) } else { - result = db!.executeQuery(queryBackwardFilterMore, query + "%", "% " + query + "%", sortingKey!.toNSNumber(), limit.toNSNumber()); + result = db!.executeQuery(queryBackwardFilterMore, query + "%", "% " + query + "%", sortingKey!.toNSNumber(), limit.toNSNumber()) } if (result == nil) { NSLog(db!.lastErrorMessage()) @@ -321,28 +321,28 @@ class FMDBList : NSObject, ARListStorageDisplayEx { let res: JavaUtilArrayList = JavaUtilArrayList(); while(result!.next()) { - var query: AnyObject! = result!.objectForColumnName("QUERY"); + var query: AnyObject! = result!.object(forColumnName: "QUERY") as AnyObject!; if (query is NSNull) { query = nil } - let record = ARListEngineRecord(key: jlong(result!.longLongIntForColumn("ID")), withOrder: jlong(result!.longLongIntForColumn("SORT_KEY")), withQuery: query as! String?, withData: result!.dataForColumn("BYTES").toJavaBytes()) - res.addWithId(record) + let record = ARListEngineRecord(key: jlong(result!.longLongInt(forColumn: "ID")), withOrder: jlong(result!.longLongInt(forColumn: "SORT_KEY")), withQuery: query as! String?, withData: result!.data(forColumn: "BYTES").toJavaBytes()) + res.add(withId: record) } result!.close() return res; } - func loadCenterWithSortKey(centerSortKey: JavaLangLong!, withLimit limit: jint) -> JavaUtilList! { + func loadCenter(withSortKey centerSortKey: JavaLangLong!, withLimit limit: jint) -> JavaUtilList! { checkTable(); let res: JavaUtilArrayList = JavaUtilArrayList(); - res.addAllWithJavaUtilCollection(loadSlise(db!.executeQuery(queryCenterBackward, centerSortKey.toNSNumber(), limit.toNSNumber()))) - res.addAllWithJavaUtilCollection(loadSlise(db!.executeQuery(queryCenterForward, centerSortKey.toNSNumber(), limit.toNSNumber()))) + res.addAll(with: loadSlise(db!.executeQuery(queryCenterBackward, centerSortKey.toNSNumber(), limit.toNSNumber()))) + res.addAll(with: loadSlise(db!.executeQuery(queryCenterForward, centerSortKey.toNSNumber(), limit.toNSNumber()))) return res } - func loadSlise(result: FMResultSet?) -> JavaUtilList! { + func loadSlise(_ result: FMResultSet?) -> JavaUtilList! { if (result == nil) { NSLog(db!.lastErrorMessage()) return nil @@ -351,14 +351,14 @@ class FMDBList : NSObject, ARListStorageDisplayEx { let res: JavaUtilArrayList = JavaUtilArrayList(); while(result!.next()) { - var query: AnyObject! = result!.objectForColumnName("QUERY"); + var query: AnyObject! = result!.object(forColumnName: "QUERY") as AnyObject!; if (query is NSNull) { query = nil } - let record = ARListEngineRecord(key: jlong(result!.longLongIntForColumn("ID")), withOrder: jlong(result!.longLongIntForColumn("SORT_KEY")), withQuery: query as! String?, withData: result!.dataForColumn("BYTES").toJavaBytes()) - res.addWithId(record) + let record = ARListEngineRecord(key: jlong(result!.longLongInt(forColumn: "ID")), withOrder: jlong(result!.longLongInt(forColumn: "SORT_KEY")), withQuery: query as! String?, withData: result!.data(forColumn: "BYTES").toJavaBytes()) + res.add(withId: record) } result!.close() return res; } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/UDPreferencesStorage.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/UDPreferencesStorage.swift index 72d8e1d9a7..0ee7e3adba 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/UDPreferencesStorage.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/Storage/UDPreferencesStorage.swift @@ -6,41 +6,41 @@ import Foundation @objc class UDPreferencesStorage: NSObject, ARPreferencesStorage { - let prefs = NSUserDefaults.standardUserDefaults() + let prefs = UserDefaults.standard var cachedPrefs = [String: AnyObject?]() - func putLongWithKey(key: String!, withValue v: jlong) { - setObject(key, obj: NSNumber(longLong: v)) + func putLong(withKey key: String!, withValue v: jlong) { + setObject(key, obj: NSNumber(value: v as Int64)) } - func getLongWithKey(key: String!, withDefault def: jlong) -> jlong { + func getLongWithKey(_ key: String!, withDefault def: jlong) -> jlong { let val = fetchObj(key) if (val == nil || !(val is NSNumber)) { return def; } else { - return (val as! NSNumber).longLongValue + return (val as! NSNumber).int64Value } } - func putIntWithKey(key: String!, withValue v: jint) { - setObject(key, obj: Int(v)) + func putInt(withKey key: String!, withValue v: jint) { + setObject(key, obj: Int(v) as AnyObject?) } - func getIntWithKey(key: String!, withDefault def: jint) -> jint { + func getIntWithKey(_ key: String!, withDefault def: jint) -> jint { let val: AnyObject? = fetchObj(key) if (val == nil || !(val is NSNumber)) { return def; } else { - return (val as! NSNumber).intValue + return (val as! NSNumber).int32Value } } - func putBoolWithKey(key: String!, withValue v: Bool) { - setObject(key, obj: v) + func putBool(withKey key: String!, withValue v: Bool) { + setObject(key, obj: v as AnyObject?) } - func getBoolWithKey(key: String!, withDefault def: Bool) -> Bool { + func getBoolWithKey(_ key: String!, withDefault def: Bool) -> Bool { let val: AnyObject? = fetchObj(key); if (val == nil || (!(val is Bool))) { return def @@ -49,28 +49,28 @@ import Foundation } } - func putBytesWithKey(key: String!, withValue v: IOSByteArray!) { + func putBytes(withKey key: String!, withValue v: IOSByteArray!) { if (v == nil) { setObject(key, obj: nil) } else { - setObject(key, obj: v.toNSData()) + setObject(key, obj: v.toNSData() as AnyObject?) } } - func getBytesWithKey(key: String!) -> IOSByteArray! { + func getBytesWithKey(_ key: String!) -> IOSByteArray! { let val = fetchObj(key); if (val == nil || !(val is NSData)){ return nil } else { - return (val as! NSData).toJavaBytes() + return (val as! Data).toJavaBytes() } } - func putStringWithKey(key: String!, withValue v: String!) { - setObject(key, obj: v) + func putString(withKey key: String!, withValue v: String!) { + setObject(key, obj: v as AnyObject?) } - func getStringWithKey(key: String!) -> String! { + func getStringWithKey(_ key: String!) -> String! { let val = fetchObj(key); if (val == nil || !(val is String)) { return nil @@ -80,8 +80,8 @@ import Foundation } func clear() { - let appDomain = NSBundle.mainBundle().bundleIdentifier! - prefs.removePersistentDomainForName(appDomain) + let appDomain = Bundle.main.bundleIdentifier! + prefs.removePersistentDomain(forName: appDomain) } @@ -89,23 +89,23 @@ import Foundation // Interface // - private func setObject(key: String, obj: AnyObject?) { + fileprivate func setObject(_ key: String, obj: AnyObject?) { if obj != nil { - prefs.setObject(obj, forKey: key) + prefs.set(obj, forKey: key) cachedPrefs[key] = obj } else { - prefs.removeObjectForKey(key) - cachedPrefs.removeValueForKey(key) + prefs.removeObject(forKey: key) + cachedPrefs.removeValue(forKey: key) } prefs.synchronize() } - private func fetchObj(key: String) -> AnyObject? { + fileprivate func fetchObj(_ key: String) -> AnyObject? { if let obj = cachedPrefs[key] { return obj } - let res = prefs.objectForKey(key) - cachedPrefs[key] = res - return res + let res = prefs.object(forKey: key) + cachedPrefs[key] = res as AnyObject?? + return res as AnyObject? } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/iOSCallsProvider.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/iOSCallsProvider.swift index e2e1150555..574a232d28 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/iOSCallsProvider.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/iOSCallsProvider.swift @@ -11,27 +11,27 @@ class iOSCallsProvider: NSObject, ACCallsProvider { var ringtonePlayer: AVAudioPlayer! = nil var latestNotification: UILocalNotification! - func onCallStartWithCallId(callId: jlong) { + func onCallStart(withCallId callId: jlong) { AAAudioManager.sharedAudio().callStart(Actor.getCallWithCallId(callId)) dispatchOnUi() { let rootController = ActorSDK.sharedActor().bindedToWindow.rootViewController! if let presented = rootController.presentedViewController { - presented.dismissViewControllerAnimated(true, completion: { () -> Void in - rootController.presentViewController(AACallViewController(callId: callId), animated: true, completion: nil) + presented.dismiss(animated: true, completion: { () -> Void in + rootController.present(AACallViewController(callId: callId), animated: true, completion: nil) }) } else { - rootController.presentViewController(AACallViewController(callId: callId), animated: true, completion: nil) + rootController.present(AACallViewController(callId: callId), animated: true, completion: nil) } } } - func onCallAnsweredWithCallId(callId: jlong) { + func onCallAnswered(withCallId callId: jlong) { AAAudioManager.sharedAudio().callAnswered(Actor.getCallWithCallId(callId)) } - func onCallEndWithCallId(callId: jlong) { + func onCallEnd(withCallId callId: jlong) { AAAudioManager.sharedAudio().callEnd(Actor.getCallWithCallId(callId)) } @@ -39,7 +39,7 @@ class iOSCallsProvider: NSObject, ACCallsProvider { if (beepPlayer == nil) { do { - beepPlayer = try AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: NSBundle.framework.pathForResource("tone", ofType: "m4a")!)) + beepPlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: Bundle.framework.path(forResource: "tone", ofType: "m4a")!)) beepPlayer.prepareToPlay() beepPlayer.numberOfLoops = -1 } catch let error as NSError { @@ -56,4 +56,4 @@ class iOSCallsProvider: NSObject, ACCallsProvider { beepPlayer = nil } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/iOSNotificationProvider.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/iOSNotificationProvider.swift index b7ea0f23e7..ec1729a4c5 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/iOSNotificationProvider.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/iOSNotificationProvider.swift @@ -17,25 +17,25 @@ import AudioToolbox.AudioServices super.init() } - func loadSound(soundFile:String? = ""){ + func loadSound(_ soundFile:String? = ""){ if !isLoaded { isLoaded = true - var path = NSBundle.framework.URLForResource("notification", withExtension: "caf") + var path = Bundle.framework.url(forResource: "notification", withExtension: "caf") - if let fileURL: NSURL = NSURL(fileURLWithPath: "/Library/Ringtones/\(soundFile)") { - path = fileURL + if let fileURL: URL = URL(fileURLWithPath: "/Library/Ringtones/\(soundFile)") { + path = fileURL } - AudioServicesCreateSystemSoundID(path!, &internalMessage) + AudioServicesCreateSystemSoundID(path! as CFURL, &internalMessage) } } - func onMessageArriveInAppWithMessenger(messenger: ACMessenger!) { - let currentTime = NSDate().timeIntervalSinceReferenceDate + func onMessageArriveInApp(with messenger: ACMessenger!) { + let currentTime = Date().timeIntervalSinceReferenceDate if (currentTime - lastSoundPlay > 0.2) { - let peer = ACPeer.userWithInt(jint(messenger.myUid())) - let soundFileSting = messenger.getNotificationsSoundWithPeer(peer) + let peer = ACPeer.user(with: jint(messenger.myUid())) + let soundFileSting = messenger.getNotificationsSound(with: peer) loadSound(soundFileSting) AudioServicesPlaySystemSound(internalMessage) lastSoundPlay = currentTime @@ -43,11 +43,11 @@ import AudioToolbox.AudioServices } - func onNotificationWithMessenger(messenger: ACMessenger!, withTopNotifications topNotifications: JavaUtilList!, withMessagesCount messagesCount: jint, withConversationsCount conversationsCount: jint) { + func onNotification(with messenger: ACMessenger!, withTopNotifications topNotifications: JavaUtilList!, withMessagesCount messagesCount: jint, withConversationsCount conversationsCount: jint) { // Not Supported } - func onUpdateNotificationWithMessenger(messenger: ACMessenger!, withTopNotifications topNotifications: JavaUtilList!, withMessagesCount messagesCount: jint, withConversationsCount conversationsCount: jint) { + func onUpdateNotification(with messenger: ACMessenger!, withTopNotifications topNotifications: JavaUtilList!, withMessagesCount messagesCount: jint, withConversationsCount conversationsCount: jint) { // Not Supported } @@ -55,15 +55,15 @@ import AudioToolbox.AudioServices dispatchOnUi { () -> Void in // Clearing notifications if let number = Actor.getGlobalState().globalCounter.get() { - UIApplication.sharedApplication().applicationIconBadgeNumber = 0 // If current value will equals to number + 1 - UIApplication.sharedApplication().applicationIconBadgeNumber = number.integerValue + 1 - UIApplication.sharedApplication().applicationIconBadgeNumber = number.integerValue + UIApplication.shared.applicationIconBadgeNumber = 0 // If current value will equals to number + 1 + UIApplication.shared.applicationIconBadgeNumber = number.intValue + 1 + UIApplication.shared.applicationIconBadgeNumber = number.intValue } else { - UIApplication.sharedApplication().applicationIconBadgeNumber = 0 + UIApplication.shared.applicationIconBadgeNumber = 0 } // Clearing local notifications - UIApplication.sharedApplication().cancelAllLocalNotifications() + UIApplication.shared.cancelAllLocalNotifications() } } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/iOSPhoneBookProvider.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/iOSPhoneBookProvider.swift index 435d39ff13..771416d65a 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/iOSPhoneBookProvider.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorCore/Providers/iOSPhoneBookProvider.swift @@ -7,127 +7,127 @@ import AddressBook class PhoneBookProvider: NSObject, ACPhoneBookProvider { - func loadPhoneBookWithCallback(callback: ACPhoneBookProvider_Callback!) { + func loadPhoneBook(with callback: ACPhoneBookProvider_Callback!) { dispatchBackgroundDelayed(5.0) { let rawBook = ABAddressBookCreateWithOptions(nil, nil) if (rawBook == nil) { print("Access to AddressBook denied") - callback.onLoadedWithContacts(JavaUtilArrayList()) + callback.onLoaded(withContacts: JavaUtilArrayList()) return } - let book: ABAddressBook = rawBook.takeRetainedValue() - ABAddressBookRequestAccessWithCompletion(book, { (granted: Bool, error: CFError!) -> Void in - if (!granted) { - print("Access to AddressBook denied") - callback.onLoadedWithContacts(JavaUtilArrayList()) - return - } - - autoreleasepool { - let numbersSet = NSCharacterSet(charactersInString: "0123456789").invertedSet - let contacts = JavaUtilArrayList() - var index = 1 - let people = ABAddressBookCopyArrayOfAllPeople(book).takeRetainedValue() as [ABRecordRef] - - for person in people { - let firstName = self.extractString(person as ABRecord, propertyName: kABPersonFirstNameProperty) - let middleName = self.extractString(person as ABRecord, propertyName: kABPersonMiddleNameProperty) - let lastName = self.extractString(person as ABRecord, propertyName: kABPersonLastNameProperty) - - var contactName :String? - - // - // For Performance. LOL. - // - if firstName != nil { - if middleName != nil { - if lastName != nil { - contactName = firstName! + " " + middleName! + " " + lastName! - } else { - contactName = firstName! + " " + middleName! - } - } else { - if (lastName != nil) { - contactName = firstName! + " " + lastName! - } else { - contactName = firstName - } - } - } else { - if middleName != nil { - if lastName != nil { - contactName = middleName! + " " + lastName! - } else { - contactName = middleName - } - } else { - if lastName != nil { - contactName = lastName - } - } - } - - if (firstName == "Name not specified") { - contactName = nil - } - - let contactPhones = JavaUtilArrayList() - let contactEmails = JavaUtilArrayList() - let contact = ACPhoneBookContact(long: jlong(index), withNSString: contactName, withJavaUtilList: contactPhones, withJavaUtilList: contactEmails) - index += 1 - if let phones: ABMultiValueRef = - self.extractProperty(person as ABRecord, propertyName: kABPersonPhoneProperty) as ABMultiValueRef? { - for i in 0...ABMultiValueGetCount(phones) { - var phoneStr = self.extractString(phones, index: i) - if (phoneStr == nil || phoneStr!.trim().isEmpty) { - continue - } - phoneStr = phoneStr!.strip(numbersSet) - let phoneVal = Int64(phoneStr!)// numberFormatter.numberFromString(phoneStr!)?.longLongValue - if (phoneVal != nil) { - contactPhones.addWithId(ACPhoneBookPhone(long: jlong(index), withLong: jlong(phoneVal!))) - index += 1 - } - } - } - - if let emails: ABMultiValueRef = - self.extractProperty(person as ABRecord, propertyName: kABPersonEmailProperty) as ABMultiValueRef? { - for i in 0...ABMultiValueGetCount(emails) { - let emailStr = self.extractString(emails, index: i) - if (emailStr == nil || emailStr!.trim().isEmpty) { - continue - } - contactEmails.addWithId(ACPhoneBookEmail(long: jlong(index), withNSString: emailStr!)) - index += 1 - } - } - - if (contactPhones.size() != 0 || contactEmails.size() != 0) { - contacts.addWithId(contact) - } - } - - callback.onLoadedWithContacts(contacts) - } - }) + let book: ABAddressBook = rawBook!.takeRetainedValue() +// ABAddressBookRequestAccessWithCompletion(book, { (granted: Bool, error: CFError!) -> Void in +// if (!granted) { +// print("Access to AddressBook denied") +// callback.onLoaded(withContacts: JavaUtilArrayList()) +// return +// } +// +// autoreleasepool { +// let numbersSet = CharacterSet(charactersIn: "0123456789").inverted +// let contacts = JavaUtilArrayList() +// var index = 1 +// let people = ABAddressBookCopyArrayOfAllPeople(book).takeRetainedValue() as [ABRecordRef] +// +// for person in people { +// let firstName = self.extractString(person as ABRecord, propertyName: kABPersonFirstNameProperty) +// let middleName = self.extractString(person as ABRecord, propertyName: kABPersonMiddleNameProperty) +// let lastName = self.extractString(person as ABRecord, propertyName: kABPersonLastNameProperty) +// +// var contactName :String? +// +// // +// // For Performance. LOL. +// // +// if firstName != nil { +// if middleName != nil { +// if lastName != nil { +// contactName = firstName! + " " + middleName! + " " + lastName! +// } else { +// contactName = firstName! + " " + middleName! +// } +// } else { +// if (lastName != nil) { +// contactName = firstName! + " " + lastName! +// } else { +// contactName = firstName +// } +// } +// } else { +// if middleName != nil { +// if lastName != nil { +// contactName = middleName! + " " + lastName! +// } else { +// contactName = middleName +// } +// } else { +// if lastName != nil { +// contactName = lastName +// } +// } +// } +// +// if (firstName == "Name not specified") { +// contactName = nil +// } +// +// let contactPhones = JavaUtilArrayList() +// let contactEmails = JavaUtilArrayList() +// let contact = ACPhoneBookContact(long: jlong(index), with: contactName, with: contactPhones, with: contactEmails) +// index += 1 +// if let phones: ABMultiValueRef = +// self.extractProperty(person as ABRecord, propertyName: kABPersonPhoneProperty) as ABMultiValueRef? { +// for i in 0...ABMultiValueGetCount(phones) { +// var phoneStr = self.extractString(phones, index: i) +// if (phoneStr == nil || phoneStr!.trim().isEmpty) { +// continue +// } +// phoneStr = phoneStr!.strip(numbersSet) +// let phoneVal = Int64(phoneStr!)// numberFormatter.numberFromString(phoneStr!)?.longLongValue +// if (phoneVal != nil) { +// contactPhones.addWithId(ACPhoneBookPhone(long: jlong(index), withLong: jlong(phoneVal!))) +// index += 1 +// } +// } +// } +// +// if let emails: ABMultiValueRef = +// self.extractProperty(person as ABRecord, propertyName: kABPersonEmailProperty) as ABMultiValueRef? { +// for i in 0...ABMultiValueGetCount(emails) { +// let emailStr = self.extractString(emails, index: i) +// if (emailStr == nil || emailStr!.trim().isEmpty) { +// continue +// } +// contactEmails.addWithId(ACPhoneBookEmail(long: jlong(index), with: emailStr!)) +// index += 1 +// } +// } +// +// if (contactPhones.size() != 0 || contactEmails.size() != 0) { +// contacts.addWithId(contact) +// } +// } +// +// callback.onLoaded(withContacts: contacts) +// } +// }) } } - private func extractString(record: ABRecord, propertyName : ABPropertyID) -> String? { + fileprivate func extractString(_ record: ABRecord, propertyName : ABPropertyID) -> String? { return extractProperty(record, propertyName: propertyName) } - private func extractProperty(record: ABRecord, propertyName : ABPropertyID) -> T? { + fileprivate func extractProperty(_ record: ABRecord, propertyName : ABPropertyID) -> T? { //the following is two-lines of code for a reason. Do not combine (compiler optimization problems) let value: AnyObject? = ABRecordCopyValue(record, propertyName)?.takeRetainedValue() return value as? T } - private func extractString(record: ABMultiValueRef, index: Int) -> String? { + fileprivate func extractString(_ record: ABMultiValue, index: Int) -> String? { let value: AnyObject? = ABMultiValueCopyValueAtIndex(record, index)?.takeRetainedValue() return value as? String } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDK.h b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDK.h index af900c424c..d9f943826d 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDK.h +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDK.h @@ -30,27 +30,32 @@ FOUNDATION_EXPORT const unsigned char ActorSDKVersionString[]; #import "FMDatabaseAdditions.h" #import "FMDatabaseQueue.h" -#import "CLTokenInputView.h" +// GCDAsyncSocket -#import "CLTokenInputView.h" +#import "GCDAsyncSocket.h" // Ogg record +#import "AAAudioRecorder.h" +#import "AAAudioPlayer.h" +#import "AAModernConversationAudioPlayer.h" + +// SLKTextViewController + #import "SLKTextViewController.h" +// NYTPhotos + #import "NYTPhotosViewController.h" #import "NYTPhoto.h" #import "NYTPhotoViewController.h" #import "NYTPhotosViewControllerDataSource.h" #import "NYTPhotoCaptionViewLayoutWidthHinting.h" -#import "AAAudioRecorder.h" -#import "AAAudioPlayer.h" -#import "AAModernConversationAudioPlayer.h" - // CLTokenView #import "CLTokenView.h" +#import "CLTokenInputView.h" // YYKit diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDK.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDK.swift index 0c7bd11c27..a91ef5ab1e 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDK.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDK.swift @@ -7,16 +7,17 @@ import JDStatusBarNotification import PushKit import SafariServices import DZNWebViewController +import ReachabilitySwift -@objc public class ActorSDK: NSObject, PKPushRegistryDelegate { +@objc open class ActorSDK: NSObject, PKPushRegistryDelegate { // // Shared instance // - private static let shared = ActorSDK() + fileprivate static let shared = ActorSDK() - public static func sharedActor() -> ActorSDK { + open static func sharedActor() -> ActorSDK { return shared } @@ -25,23 +26,23 @@ import DZNWebViewController // /// Main Messenger object - public var messenger : ACCocoaMessenger! + open var messenger : ACCocoaMessenger! // Actor Style - public let style = ActorStyle() + open let style = ActorStyle() /// SDK Delegate - public var delegate: ActorSDKDelegate = ActorSDKDelegateDefault() + open var delegate: ActorSDKDelegate = ActorSDKDelegateDefault() /// SDK Analytics - public var analyticsDelegate: ActorSDKAnalytics? + open var analyticsDelegate: ActorSDKAnalytics? // // Configuration // /// Server Endpoints - public var endpoints = [ + open var endpoints = [ "tcp://front1-mtproto-api-rev3.actor.im:443", "tcp://front2-mtproto-api-rev3.actor.im:443" ] { @@ -51,7 +52,7 @@ import DZNWebViewController } /// Trusted Server Keys - public var trustedKeys = [ + open var trustedKeys = [ "d9d34ed487bd5b434eda2ef2c283db587c3ae7fb88405c3834d9d1a6d247145b", "4bd5422b50c585b5c8575d085e9fae01c126baa968dab56a396156759d5a7b46", "ff61103913aed3a9a689b6d77473bc428d363a3421fdd48a8e307a08e404f02c", @@ -61,87 +62,98 @@ import DZNWebViewController ] /// API ID - public var apiId = 2 + open var apiId = 2 /// API Key - public var apiKey = "2ccdc3699149eac0a13926c77ca84e504afd68b4f399602e06d68002ace965a3" + open var apiKey = "2ccdc3699149eac0a13926c77ca84e504afd68b4f399602e06d68002ace965a3" /// Push registration mode - public var autoPushMode = AAAutoPush.AfterLogin + open var autoPushMode = AAAutoPush.afterLogin /// Push token registration id. Required for sending push tokens - public var apiPushId: Int? = nil + open var apiPushId: Int? = nil /// Strategy about authentication - public var authStrategy = AAAuthStrategy.PhoneOnly + open var authStrategy = AAAuthStrategy.phoneOnly /// Enable phone book import - public var enablePhoneBookImport = true + open var enablePhoneBookImport = true /// Invitation URL for apps - public var inviteUrl: String = "https://actor.im/dl" + open var inviteUrl: String = "https://actor.im/dl" + + /// Invitation URL for apps + open var invitePrefix: String? = "https://actor.im/join/" + + /// Invitation URL for apps + open var invitePrefixShort: String? = "actor.im/join/" /// Privacy Policy URL - public var privacyPolicyUrl: String? = nil + open var privacyPolicyUrl: String? = nil /// Privacy Policy Text - public var privacyPolicyText: String? = nil + open var privacyPolicyText: String? = nil /// Terms of Service URL - public var termsOfServiceUrl: String? = nil + open var termsOfServiceUrl: String? = nil /// Terms of Service Text - public var termsOfServiceText: String? = nil + open var termsOfServiceText: String? = nil /// App name - public var appName: String = "Actor" + open var appName: String = "Actor" /// Use background on welcome screen - public var useBackgroundOnWelcomeScreen: Bool? = false + open var useBackgroundOnWelcomeScreen: Bool? = false /// Support email - public var supportEmail: String? = nil + open var supportEmail: String? = nil /// Support email - public var supportActivationEmail: String? = nil + open var supportActivationEmail: String? = nil /// Support account - public var supportAccount: String? = nil + open var supportAccount: String? = nil /// Support home page - public var supportHomepage: String? = "https://actor.im" + open var supportHomepage: String? = "https://actor.im" /// Support account - public var supportTwitter: String? = "actorapp" + open var supportTwitter: String? = "actorapp" /// Invite url scheme - public var inviteUrlScheme: String? = nil + open var inviteUrlScheme: String? = nil /// Web Invite Domain host - public var inviteUrlHost: String? = nil + open var inviteUrlHost: String? = nil /// Enable voice calls feature - public var enableCalls: Bool = false + open var enableCalls: Bool = false /// Enable video calls feature - public var enableVideoCalls: Bool = false + open var enableVideoCalls: Bool = false /// Enable custom sound on Groups and Chats - public var enableChatGroupSound: Bool = false + open var enableChatGroupSound: Bool = false /// Enable experimental features - public var enableExperimentalFeatures: Bool = false + open var enableExperimentalFeatures: Bool = false + + /// Auto Join Groups + open var autoJoinGroups = [String]() + /// Should perform auto join only after first message or contact + open var autoJoinOnReady = true // // User Onlines // /// Is User online - private(set) public var isUserOnline = false + fileprivate(set) open var isUserOnline = false /// Disable this if you want manually handle online states - public var automaticOnlineHandling = true + open var automaticOnlineHandling = true // @@ -149,35 +161,35 @@ import DZNWebViewController // // Local Shared Settings - private static var udStorage = UDPreferencesStorage() + fileprivate static var udStorage = UDPreferencesStorage() - public var isPhotoAutoDownloadGroup: Bool = udStorage.getBoolWithKey("local.photo_download.group", withDefault: true) { + open var isPhotoAutoDownloadGroup: Bool = udStorage.getBoolWithKey("local.photo_download.group", withDefault: true) { willSet(v) { - ActorSDK.udStorage.putBoolWithKey("local.photo_download.group", withValue: v) + ActorSDK.udStorage.putBool(withKey: "local.photo_download.group", withValue: v) } } - public var isPhotoAutoDownloadPrivate: Bool = udStorage.getBoolWithKey("local.photo_download.private", withDefault: true) { + open var isPhotoAutoDownloadPrivate: Bool = udStorage.getBoolWithKey("local.photo_download.private", withDefault: true) { willSet(v) { - ActorSDK.udStorage.putBoolWithKey("local.photo_download.private", withValue: v) + ActorSDK.udStorage.putBool(withKey: "local.photo_download.private", withValue: v) } } - public var isAudioAutoDownloadGroup: Bool = udStorage.getBoolWithKey("local.audio_download.group", withDefault: true) { + open var isAudioAutoDownloadGroup: Bool = udStorage.getBoolWithKey("local.audio_download.group", withDefault: true) { willSet(v) { - ActorSDK.udStorage.putBoolWithKey("local.audio_download.group", withValue: v) + ActorSDK.udStorage.putBool(withKey: "local.audio_download.group", withValue: v) } } - public var isAudioAutoDownloadPrivate: Bool = udStorage.getBoolWithKey("local.audio_download.private", withDefault: true) { + open var isAudioAutoDownloadPrivate: Bool = udStorage.getBoolWithKey("local.audio_download.private", withDefault: true) { willSet(v) { - ActorSDK.udStorage.putBoolWithKey("local.audio_download.private", withValue: v) + ActorSDK.udStorage.putBool(withKey: "local.audio_download.private", withValue: v) } } - public var isGIFAutoplayEnabled: Bool = udStorage.getBoolWithKey("local.autoplay_gif", withDefault: true) { + open var isGIFAutoplayEnabled: Bool = udStorage.getBoolWithKey("local.autoplay_gif", withDefault: true) { willSet(v) { - ActorSDK.udStorage.putBoolWithKey("local.autoplay_gif", withValue: v) + ActorSDK.udStorage.putBool(withKey: "local.autoplay_gif", withValue: v) } } @@ -187,27 +199,27 @@ import DZNWebViewController // /// Is Actor Started - private(set) public var isStarted = false + fileprivate(set) open var isStarted = false - private var binder = AABinder() - private var syncTask: UIBackgroundTaskIdentifier? - private var completionHandler: ((UIBackgroundFetchResult) -> Void)? + fileprivate var binder = AABinder() + fileprivate var syncTask: UIBackgroundTaskIdentifier? + fileprivate var completionHandler: ((UIBackgroundFetchResult) -> Void)? // View Binding info - private(set) public var bindedToWindow: UIWindow! + fileprivate(set) open var bindedToWindow: UIWindow! // Reachability - private var reachability: Reachability! + fileprivate var reachability: Reachability! public override init() { // Auto Loading Application name - if let name = NSBundle.mainBundle().objectForInfoDictionaryKey(String(kCFBundleNameKey)) as? String { + if let name = Bundle.main.object(forInfoDictionaryKey: String(kCFBundleNameKey)) as? String { self.appName = name } } - public func createActor() { + open func createActor() { if isStarted { return @@ -216,11 +228,11 @@ import DZNWebViewController AAActorRuntime.configureRuntime() - let builder = ACConfigurationBuilder() + let builder = ACConfigurationBuilder()! // Api Connections - let deviceKey = NSUUID().UUIDString - let deviceName = UIDevice.currentDevice().name + let deviceKey = UUID().uuidString + let deviceName = UIDevice.current.name let appTitle = "Actor iOS" for url in endpoints { builder.addEndpoint(url) @@ -236,20 +248,30 @@ import DZNWebViewController builder.setCallsProvider(iOSCallsProvider()) // Stats - builder.setPlatformType(ACPlatformType.IOS()) - builder.setDeviceCategory(ACDeviceCategory.MOBILE()) + builder.setPlatformType(ACPlatformType.ios()) + builder.setDeviceCategory(ACDeviceCategory.mobile()) // Locale - for lang in NSLocale.preferredLanguages() { + for lang in Locale.preferredLanguages { log("Found locale :\(lang)") builder.addPreferredLanguage(lang) } // TimeZone - let timeZone = NSTimeZone.defaultTimeZone().name + let timeZone = TimeZone.current.identifier log("Found time zone :\(timeZone)") builder.setTimeZone(timeZone) + // AutoJoin + for s in autoJoinGroups { + builder.addAutoJoinGroup(withToken: s) + } + if autoJoinOnReady { + builder.setAutoJoinType(ACAutoJoinType.after_INIT()) + } else { + builder.setAutoJoinType(ACAutoJoinType.immediately()) + } + // Logs // builder.setEnableFilesLogging(true) @@ -276,17 +298,17 @@ import DZNWebViewController binder.bind(messenger.getGlobalState().isSyncing, closure: { (value: JavaLangBoolean?) -> () in if value!.booleanValue() { if self.syncTask == nil { - self.syncTask = UIApplication.sharedApplication().beginBackgroundTaskWithName("Background Sync", expirationHandler: { () -> Void in + self.syncTask = UIApplication.shared.beginBackgroundTask(withName: "Background Sync", expirationHandler: { () -> Void in }) } } else { if self.syncTask != nil { - UIApplication.sharedApplication().endBackgroundTask(self.syncTask!) + UIApplication.shared.endBackgroundTask(self.syncTask!) self.syncTask = nil } if self.completionHandler != nil { - self.completionHandler!(UIBackgroundFetchResult.NewData) + self.completionHandler!(UIBackgroundFetchResult.newData) self.completionHandler = nil } } @@ -296,35 +318,33 @@ import DZNWebViewController binder.bind(Actor.getGlobalState().globalCounter, closure: { (value: JavaLangInteger?) -> () in if let v = value { - UIApplication.sharedApplication().applicationIconBadgeNumber = Int(v.integerValue) + UIApplication.shared.applicationIconBadgeNumber = Int(v.intValue) } else { - UIApplication.sharedApplication().applicationIconBadgeNumber = 0 + UIApplication.shared.applicationIconBadgeNumber = 0 } }) // Push registration - if autoPushMode == .FromStart { + if autoPushMode == .fromStart { requestPush() } // Subscribe to network changes - do { - reachability = try Reachability.reachabilityForInternetConnection() - NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ActorSDK.reachabilityChanged(_:)), name: ReachabilityChangedNotification, object: reachability) - try reachability.startNotifier() - } catch { + reachability = Reachability()! + if reachability != nil { + reachability.whenReachable = { reachability in + self.messenger.forceNetworkCheck() + } + + do { + try reachability.startNotifier() + } catch { + print("Unable to start Reachability") + } + } else { print("Unable to create Reachability") - return - } - } - - @objc func reachabilityChanged(note: NSNotification) { - print("reachabilityChanged (\(reachability.isReachable()))") - - if reachability.isReachable() { - messenger.forceNetworkCheck() } } @@ -332,7 +352,7 @@ import DZNWebViewController // Push registration - if autoPushMode == .AfterLogin { + if autoPushMode == .afterLogin { requestPush() } @@ -367,56 +387,56 @@ import DZNWebViewController // /// Token need to be with stripped everything except numbers and letters - func pushRegisterToken(token: String) { + func pushRegisterToken(_ token: String) { if !isStarted { fatalError("Messenger not started") } if apiPushId != nil { - messenger.registerApplePushWithApnsId(jint(apiPushId!), withToken: token) + messenger.registerApplePush(withApnsId: jint(apiPushId!), withToken: token) } } - func pushRegisterKitToken(token: String) { + func pushRegisterKitToken(_ token: String) { if !isStarted { fatalError("Messenger not started") } if apiPushId != nil { - messenger.registerApplePushKitWithApnsId(jint(apiPushId!), withToken: token) + messenger.registerApplePushKit(withApnsId: jint(apiPushId!), withToken: token) } } - private func requestPush() { - let types: UIUserNotificationType = [.Alert, .Badge, .Sound] - let settings: UIUserNotificationSettings = UIUserNotificationSettings(forTypes: types, categories: nil) - UIApplication.sharedApplication().registerUserNotificationSettings(settings) - UIApplication.sharedApplication().registerForRemoteNotifications() + fileprivate func requestPush() { + let types: UIUserNotificationType = [.alert, .badge, .sound] + let settings: UIUserNotificationSettings = UIUserNotificationSettings(types: types, categories: nil) + UIApplication.shared.registerUserNotificationSettings(settings) + UIApplication.shared.registerForRemoteNotifications() } - private func requestPushKit() { - let voipRegistry = PKPushRegistry(queue: dispatch_get_main_queue()) + fileprivate func requestPushKit() { + let voipRegistry = PKPushRegistry(queue: DispatchQueue.main) voipRegistry.delegate = self - voipRegistry.desiredPushTypes = Set([PKPushTypeVoIP]) + voipRegistry.desiredPushTypes = Set([PKPushType.voIP]) } - @objc public func pushRegistry(registry: PKPushRegistry!, didUpdatePushCredentials credentials: PKPushCredentials!, forType type: String!) { - if (type == PKPushTypeVoIP) { + @objc open func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, forType type: PKPushType) { + if (type == PKPushType.voIP) { let tokenString = "\(credentials.token)".replace(" ", dest: "").replace("<", dest: "").replace(">", dest: "") pushRegisterKitToken(tokenString) } } - @objc public func pushRegistry(registry: PKPushRegistry!, didInvalidatePushTokenForType type: String!) { - if (type == PKPushTypeVoIP) { + @objc open func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenForType type: PKPushType) { + if (type == PKPushType.voIP) { } } - @objc public func pushRegistry(registry: PKPushRegistry!, didReceiveIncomingPushWithPayload payload: PKPushPayload!, forType type: String!) { - if (type == PKPushTypeVoIP) { + @objc open func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, forType type: PKPushType) { + if (type == PKPushType.voIP) { let aps = payload.dictionaryPayload["aps"] as! [NSString: AnyObject] if let callId = aps["callId"] as? String { if let attempt = aps["attemptIndex"] as? String { @@ -425,13 +445,13 @@ import DZNWebViewController Actor.checkCall(jlong(callId)!, withAttempt: 0) } } else if let seq = aps["seq"] as? String { - Actor.onPushReceivedWithSeq(jint(seq)!) + Actor.onPushReceived(withSeq: jint(seq)!, withAuthId: 0) } } } /// Get main navigations with check in delegate for customize from SDK - private func getMainNavigations() -> [AANavigationController] { + fileprivate func getMainNavigations() -> [AANavigationController] { let allControllers = self.delegate.actorRootControllers() @@ -488,7 +508,7 @@ import DZNWebViewController // Presenting Messenger // - public func presentMessengerInWindow(window: UIWindow) { + open func presentMessengerInWindow(_ window: UIWindow) { if !isStarted { fatalError("Messenger not started") } @@ -497,7 +517,7 @@ import DZNWebViewController if messenger.isLoggedIn() { - if autoPushMode == .AfterLogin { + if autoPushMode == .afterLogin { requestPush() } @@ -535,8 +555,8 @@ import DZNWebViewController if !style.statusBarConnectingHidden { JDStatusBarNotification.setDefaultStyle { (style) -> JDStatusBarStyle! in - style.barColor = self.style.statusBarConnectingBgColor - style.textColor = self.style.statusBarConnectingTextColor + style?.barColor = self.style.statusBarConnectingBgColor + style?.textColor = self.style.statusBarConnectingTextColor return style } @@ -546,9 +566,9 @@ import DZNWebViewController if isSyncing!.booleanValue() || isConnecting!.booleanValue() { if isConnecting!.booleanValue() { - JDStatusBarNotification.showWithStatus(AALocalized("StatusConnecting")) + JDStatusBarNotification.show(withStatus: AALocalized("StatusConnecting")) } else { - JDStatusBarNotification.showWithStatus(AALocalized("StatusSyncing")) + JDStatusBarNotification.show(withStatus: AALocalized("StatusSyncing")) } } else { JDStatusBarNotification.dismiss() @@ -558,9 +578,9 @@ import DZNWebViewController } } - public func presentMessengerInNewWindow() { - let window = UIWindow(frame: UIScreen.mainScreen().bounds); - window.backgroundColor = UIColor.whiteColor() + open func presentMessengerInNewWindow() { + let window = UIWindow(frame: UIScreen.main.bounds); + window.backgroundColor = UIColor.white presentMessengerInWindow(window) window.makeKeyAndVisible() } @@ -570,31 +590,30 @@ import DZNWebViewController // /// Handling URL Opening in application - func openUrl(url: String) { - if let u = NSURL(string: url) { + func openUrl(_ url: String) { + if let u = URL(string: url) { // Handle phone call - if (u.scheme.lowercaseString == "telprompt") { - UIApplication.sharedApplication().openURL(u) + if (u.scheme?.lowercased() == "telprompt") { + UIApplication.shared.openURL(u) return } // Handle web invite url - if (u.scheme.lowercaseString == "http" || u.scheme.lowercaseString == "https") && inviteUrlHost != nil { + if (u.scheme?.lowercased() == "http" || u.scheme?.lowercased() == "https") && inviteUrlHost != nil { if u.host == inviteUrlHost { - if let token = u.lastPathComponent { - joinGroup(token) - return - } + let token = u.lastPathComponent + joinGroup(token) + return } } // Handle custom scheme invite url - if (u.scheme.lowercaseString == inviteUrlScheme?.lowercaseString) { + if (u.scheme?.lowercased() == inviteUrlScheme?.lowercased()) { if (u.host == "invite") { - let token = u.query?.componentsSeparatedByString("=")[1] + let token = u.query?.components(separatedBy: "=")[1] if token != nil { joinGroup(token!) return @@ -602,9 +621,9 @@ import DZNWebViewController } if let bindedController = bindedToWindow?.rootViewController { - let alert = UIAlertController(title: nil, message: AALocalized("ErrorUnableToJoin"), preferredStyle: .Alert) - alert.addAction(UIAlertAction(title: AALocalized("AlertOk"), style: .Cancel, handler: nil)) - bindedController.presentViewController(alert, animated: true, completion: nil) + let alert = UIAlertController(title: nil, message: AALocalized("ErrorUnableToJoin"), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: AALocalized("AlertOk"), style: .cancel, handler: nil)) + bindedController.present(alert, animated: true, completion: nil) } return @@ -617,67 +636,67 @@ import DZNWebViewController if let bindedController = bindedToWindow?.rootViewController { // Dismiss Old Presented Controller to show new one if let presented = bindedController.presentedViewController { - presented.dismissViewControllerAnimated(true, completion: nil) + presented.dismiss(animated: true, completion: nil) } // Building Controller for Web preview let controller: UIViewController if #available(iOS 9.0, *) { - controller = SFSafariViewController(URL: u) + controller = SFSafariViewController(url: u) } else { - controller = AANavigationController(rootViewController: DZNWebViewController(URL: u)) + controller = AANavigationController(rootViewController: DZNWebViewController(url: u)) } if AADevice.isiPad { - controller.modalPresentationStyle = .FullScreen + controller.modalPresentationStyle = .fullScreen } // Presenting controller - bindedController.presentViewController(controller, animated: true, completion: nil) + bindedController.present(controller, animated: true, completion: nil) } else { // Just Fallback. Might never happend - UIApplication.sharedApplication().openURL(u) + UIApplication.shared.openURL(u) } } } } /// Handling joining group by token - func joinGroup(token: String) { + func joinGroup(_ token: String) { if let bindedController = bindedToWindow?.rootViewController { - let alert = UIAlertController(title: nil, message: AALocalized("GroupJoinMessage"), preferredStyle: .Alert) - alert.addAction(UIAlertAction(title: AALocalized("AlertNo"), style: .Cancel, handler: nil)) - alert.addAction(UIAlertAction(title: AALocalized("GroupJoinAction"), style: .Default){ (action) -> Void in - AAExecutions.execute(Actor.joinGroupViaLinkCommandWithToken(token)!, type: .Safe, ignore: [], successBlock: { (val) -> Void in + let alert = UIAlertController(title: nil, message: AALocalized("GroupJoinMessage"), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: AALocalized("AlertNo"), style: .cancel, handler: nil)) + alert.addAction(UIAlertAction(title: AALocalized("GroupJoinAction"), style: .default){ (action) -> Void in + AAExecutions.execute(Actor.joinGroupViaLinkCommand(withToken: token), type: .safe, ignore: [], successBlock: { (val) -> Void in // TODO: Fix for iPad let groupId = val as! JavaLangInteger let tabBarController = bindedController as! UITabBarController let index = tabBarController.selectedIndex let navController = tabBarController.viewControllers![index] as! UINavigationController - if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer.groupWithInt(groupId.intValue)) { + if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer.group(with: groupId.int32Value)) { navController.pushViewController(customController, animated: true) } else { - navController.pushViewController(ConversationViewController(peer: ACPeer.groupWithInt(groupId.intValue)), animated: true) + navController.pushViewController(ConversationViewController(peer: ACPeer.group(with: groupId.int32Value)), animated: true) } }, failureBlock: nil) }) - bindedController.presentViewController(alert, animated: true, completion: nil) + bindedController.present(alert, animated: true, completion: nil) } } /// Tracking page visible - func trackPageVisible(page: ACPage) { + func trackPageVisible(_ page: ACPage) { analyticsDelegate?.analyticsPageVisible(page) } /// Tracking page hidden - func trackPageHidden(page: ACPage) { + func trackPageHidden(_ page: ACPage) { analyticsDelegate?.analyticsPageHidden(page) } /// Tracking event - func trackEvent(event: ACEvent) { + func trackEvent(_ event: ACEvent) { analyticsDelegate?.analyticsEvent(event) } @@ -685,7 +704,7 @@ import DZNWebViewController // File System // - public func fullFilePathForDescriptor(descriptor: String) -> String { + open func fullFilePathForDescriptor(_ descriptor: String) -> String { return CocoaFiles.pathFromDescriptor(descriptor) } @@ -693,7 +712,7 @@ import DZNWebViewController // Manual Online handling // - public func didBecameOnline() { + open func didBecameOnline() { if automaticOnlineHandling { fatalError("Manual Online handling not enabled!") @@ -709,7 +728,7 @@ import DZNWebViewController } } - public func didBecameOffline() { + open func didBecameOffline() { if automaticOnlineHandling { fatalError("Manual Online handling not enabled!") } @@ -729,7 +748,7 @@ import DZNWebViewController // func checkAppState() { - if UIApplication.sharedApplication().applicationState == .Active { + if UIApplication.shared.applicationState == .active { if !isUserOnline { isUserOnline = true @@ -761,28 +780,28 @@ import DZNWebViewController } } - public func applicationDidFinishLaunching(application: UIApplication) { + open func applicationDidFinishLaunching(_ application: UIApplication) { if !automaticOnlineHandling || !isStarted { return } checkAppState() } - public func applicationDidBecomeActive(application: UIApplication) { + open func applicationDidBecomeActive(_ application: UIApplication) { if !automaticOnlineHandling || !isStarted { return } checkAppState() } - public func applicationWillEnterForeground(application: UIApplication) { + open func applicationWillEnterForeground(_ application: UIApplication) { if !automaticOnlineHandling || !isStarted { return } checkAppState() } - public func applicationDidEnterBackground(application: UIApplication) { + open func applicationDidEnterBackground(_ application: UIApplication) { if !automaticOnlineHandling || !isStarted { return } @@ -792,20 +811,20 @@ import DZNWebViewController if messenger.isLoggedIn() { var completitionTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid - completitionTask = application.beginBackgroundTaskWithName("Completition", expirationHandler: { () -> Void in + completitionTask = application.beginBackgroundTask(withName: "Completition", expirationHandler: { () -> Void in application.endBackgroundTask(completitionTask) completitionTask = UIBackgroundTaskInvalid }) // Wait for 40 secs before app shutdown - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(40.0 * Double(NSEC_PER_SEC))), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in + DispatchQueue.global(priority: DispatchQueue.GlobalQueuePriority.default).asyncAfter(deadline: DispatchTime.now() + Double(Int64(40.0 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { () -> Void in application.endBackgroundTask(completitionTask) completitionTask = UIBackgroundTaskInvalid } } } - public func applicationWillResignActive(application: UIApplication) { + open func applicationWillResignActive(_ application: UIApplication) { if !automaticOnlineHandling || !isStarted { return } @@ -823,21 +842,21 @@ import DZNWebViewController // Handling remote notifications // - public func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) { + open func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { if !messenger.isLoggedIn() { - completionHandler(UIBackgroundFetchResult.NoData) + completionHandler(UIBackgroundFetchResult.noData) return } self.completionHandler = completionHandler } - public func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) { + open func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) { // Nothing? } - public func application(application: UIApplication, didRegisterUserNotificationSettings notificationSettings: UIUserNotificationSettings) { + open func application(_ application: UIApplication, didRegisterUserNotificationSettings notificationSettings: UIUserNotificationSettings) { requestPushKit() } @@ -845,10 +864,10 @@ import DZNWebViewController // Handling background fetch events // - public func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) { + open func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { if !messenger.isLoggedIn() { - completionHandler(UIBackgroundFetchResult.NoData) + completionHandler(UIBackgroundFetchResult.noData) return } self.completionHandler = completionHandler @@ -858,7 +877,7 @@ import DZNWebViewController // Handling invite url // - func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject) -> Bool { + func application(_ application: UIApplication, openURL url: URL, sourceApplication: String?, annotation: AnyObject) -> Bool { dispatchOnUi { () -> Void in self.openUrl(url.absoluteString) @@ -867,7 +886,7 @@ import DZNWebViewController return true } - public func application(application: UIApplication, handleOpenURL url: NSURL) -> Bool { + open func application(_ application: UIApplication, handleOpenURL url: URL) -> Bool { dispatchOnUi { () -> Void in self.openUrl(url.absoluteString) @@ -878,13 +897,13 @@ import DZNWebViewController } public enum AAAutoPush { - case None - case FromStart - case AfterLogin + case none + case fromStart + case afterLogin } public enum AAAuthStrategy { - case PhoneOnly - case EmailOnly - case PhoneEmail -} \ No newline at end of file + case phoneOnly + case emailOnly + case phoneEmail +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDKAnalytics.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDKAnalytics.swift index 317c859c25..03088fa416 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDKAnalytics.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDKAnalytics.swift @@ -7,11 +7,11 @@ import Foundation public protocol ActorSDKAnalytics { /// Called when page visible - func analyticsPageVisible(page: ACPage) + func analyticsPageVisible(_ page: ACPage) /// Called when page hidden - func analyticsPageHidden(page: ACPage) + func analyticsPageHidden(_ page: ACPage) /// Called when event occurs - func analyticsEvent(event: ACEvent) -} \ No newline at end of file + func analyticsEvent(_ event: ACEvent) +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDKDelegate.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDKDelegate.swift index d51a7edb8d..e303b1e088 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDKDelegate.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorSDKDelegate.swift @@ -17,13 +17,13 @@ public protocol ActorSDKDelegate { func actorControllerAfterLogIn() -> UIViewController? /// User profile controller - func actorControllerForUser(uid: Int) -> AAViewController? + func actorControllerForUser(_ uid: Int) -> AAViewController? /// User profile controller - func actorControllerForGroup(gid: Int) -> AAViewController? + func actorControllerForGroup(_ gid: Int) -> AAViewController? /// Conversation controller - func actorControllerForConversation(peer: ACPeer) -> UIViewController? + func actorControllerForConversation(_ peer: ACPeer) -> UIViewController? /// Contacts controller func actorControllerForContacts() -> UIViewController? @@ -41,99 +41,99 @@ public protocol ActorSDKDelegate { func actorRootInitialControllerIndex() -> Int? /// Configuration of bubble cells - func actorConfigureBubbleLayouters(builtIn: [AABubbleLayouter]) -> [AABubbleLayouter] + func actorConfigureBubbleLayouters(_ builtIn: [AABubbleLayouter]) -> [AABubbleLayouter] /// Conversation custom attach menu - func actorConversationCustomAttachMenu(controller: UIViewController) -> Bool + func actorConversationCustomAttachMenu(_ controller: UIViewController) -> Bool /// Called after header is created in settings page - func actorSettingsHeaderDidCreated(controller: AASettingsViewController, section: AAManagedSection) + func actorSettingsHeaderDidCreated(_ controller: AASettingsViewController, section: AAManagedSection) /// Called after header is created in settings page - func actorSettingsConfigurationWillCreated(controller: AASettingsViewController, section: AAManagedSection) + func actorSettingsConfigurationWillCreated(_ controller: AASettingsViewController, section: AAManagedSection) /// Called after header is created in settings page - func actorSettingsConfigurationDidCreated(controller: AASettingsViewController, section: AAManagedSection) + func actorSettingsConfigurationDidCreated(_ controller: AASettingsViewController, section: AAManagedSection) /// Called after header is created in settings page - func actorSettingsSupportWillCreated(controller: AASettingsViewController, section: AAManagedSection) + func actorSettingsSupportWillCreated(_ controller: AASettingsViewController, section: AAManagedSection) /// Called after header is created in settings page - func actorSettingsSupportDidCreated(controller: AASettingsViewController, section: AAManagedSection) + func actorSettingsSupportDidCreated(_ controller: AASettingsViewController, section: AAManagedSection) } /// Default empty implementation of SDK Delegate -public class ActorSDKDelegateDefault: NSObject, ActorSDKDelegate { +open class ActorSDKDelegateDefault: NSObject, ActorSDKDelegate { - public func actorControllerForAuthStart() -> UIViewController? { + open func actorControllerForAuthStart() -> UIViewController? { return nil } - public func actorControllerForStart() -> UIViewController? { + open func actorControllerForStart() -> UIViewController? { return nil } - public func actorControllerForUser(uid: Int) -> AAViewController? { + open func actorControllerForUser(_ uid: Int) -> AAViewController? { return nil } - public func actorControllerForGroup(gid: Int) -> AAViewController? { + open func actorControllerForGroup(_ gid: Int) -> AAViewController? { return nil } - public func actorControllerForConversation(peer: ACPeer) -> UIViewController? { + open func actorControllerForConversation(_ peer: ACPeer) -> UIViewController? { return nil } - public func actorControllerForContacts() -> UIViewController? { + open func actorControllerForContacts() -> UIViewController? { return nil } - public func actorControllerForDialogs() -> UIViewController? { + open func actorControllerForDialogs() -> UIViewController? { return nil } - public func actorControllerForSettings() -> UIViewController? { + open func actorControllerForSettings() -> UIViewController? { return nil } - public func actorRootControllers() -> [UIViewController]? { + open func actorRootControllers() -> [UIViewController]? { return nil } - public func actorRootInitialControllerIndex() -> Int? { + open func actorRootInitialControllerIndex() -> Int? { return nil } - public func actorConfigureBubbleLayouters(builtIn: [AABubbleLayouter]) -> [AABubbleLayouter] { + open func actorConfigureBubbleLayouters(_ builtIn: [AABubbleLayouter]) -> [AABubbleLayouter] { return builtIn } - public func actorControllerAfterLogIn() -> UIViewController? { + open func actorControllerAfterLogIn() -> UIViewController? { return nil } - public func actorConversationCustomAttachMenu(controller: UIViewController) -> Bool { + open func actorConversationCustomAttachMenu(_ controller: UIViewController) -> Bool { return false } - public func actorSettingsHeaderDidCreated(controller: AASettingsViewController, section: AAManagedSection) { + open func actorSettingsHeaderDidCreated(_ controller: AASettingsViewController, section: AAManagedSection) { } - public func actorSettingsConfigurationWillCreated(controller: AASettingsViewController, section: AAManagedSection) { + open func actorSettingsConfigurationWillCreated(_ controller: AASettingsViewController, section: AAManagedSection) { } - public func actorSettingsConfigurationDidCreated(controller: AASettingsViewController, section: AAManagedSection) { + open func actorSettingsConfigurationDidCreated(_ controller: AASettingsViewController, section: AAManagedSection) { } - public func actorSettingsSupportWillCreated(controller: AASettingsViewController, section: AAManagedSection) { + open func actorSettingsSupportWillCreated(_ controller: AASettingsViewController, section: AAManagedSection) { } - public func actorSettingsSupportDidCreated(controller: AASettingsViewController, section: AAManagedSection) { + open func actorSettingsSupportDidCreated(_ controller: AASettingsViewController, section: AAManagedSection) { } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorStyle.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorStyle.swift index 807674f9b6..5bf8b5e8bb 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorStyle.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ActorStyle.swift @@ -5,147 +5,147 @@ import Foundation import YYImage -public class ActorStyle { +open class ActorStyle { // // Main colors of app // /// Is Application have dark theme. Default is false. - public var isDarkApp = false + open var isDarkApp = false /// Tint Color. Star button - public var vcStarButton = UIColor(red: 75/255.0, green: 110/255.0, blue: 152/255.0, alpha: 1) + open var vcStarButton = UIColor(red: 75/255.0, green: 110/255.0, blue: 152/255.0, alpha: 1) /// Tint Color. Used for "Actions". Default is sytem blue. - public var vcTintColor = UIColor(rgb: 0x247dc7) + open var vcTintColor = UIColor(rgb: 0x247dc7) /// Color of desctructive actions. Default is red - public var vcDestructiveColor = UIColor.redColor() + open var vcDestructiveColor = UIColor.red /// Default background color - public var vcDefaultBackgroundColor = UIColor.whiteColor() + open var vcDefaultBackgroundColor = UIColor.white /// Main Text color of app - public var vcTextColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0xDE/255.0) + open var vcTextColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0xDE/255.0) /// Text Hint colors - public var vcHintColor = UIColor(red: 164/255.0, green: 164/255.0, blue: 164/255.0, alpha: 1) + open var vcHintColor = UIColor(red: 164/255.0, green: 164/255.0, blue: 164/255.0, alpha: 1) /// App's main status bar style. Default is light content. - public var vcStatusBarStyle = UIStatusBarStyle.Default + open var vcStatusBarStyle = UIStatusBarStyle.default /// UITableView separator color. Also used for other separators or borders. - public var vcSeparatorColor = UIColor(rgb: 0xdededf) + open var vcSeparatorColor = UIColor(rgb: 0xdededf) /// Cell Selected color - public var vcSelectedColor = UIColor(rgb: 0xd9d9d9) + open var vcSelectedColor = UIColor(rgb: 0xd9d9d9) /// Header/Footer text color - public var vcSectionColor = UIColor(rgb: 0x5b5a60) + open var vcSectionColor = UIColor(rgb: 0x5b5a60) /// Pacgkround of various panels like UITabBar. Default is white. - public var vcPanelBgColor = UIColor.whiteColor() + open var vcPanelBgColor = UIColor.white /// UISwitch off border color - public var vcSwitchOff = UIColor(rgb: 0xe6e6e6) + open var vcSwitchOff = UIColor(rgb: 0xe6e6e6) /// UISwitch on color - public var vcSwitchOn = UIColor(rgb: 0x4bd863) + open var vcSwitchOn = UIColor(rgb: 0x4bd863) /// View Controller background color - public var vcBgColor = UIColor.whiteColor() + open var vcBgColor = UIColor.white /// View Controller background color for settings - public var vcBackyardColor = UIColor(rgb: 0xf0eff5) + open var vcBackyardColor = UIColor(rgb: 0xf0eff5) // // UINavigationBar // /// Main Navigation bar color - public var navigationBgColor: UIColor = UIColor(red: 247.0/255.0, green: 247.0/255.0, blue: 247.0/255.0, alpha: 1) + open var navigationBgColor: UIColor = UIColor(red: 247.0/255.0, green: 247.0/255.0, blue: 247.0/255.0, alpha: 1) /// Main Navigation bar hairline color - public var navigationHairlineHidden = false + open var navigationHairlineHidden = false /// Navigation Bar icons colors - public var navigationTintColor: UIColor = UIColor(rgb: 0x5085CB) + open var navigationTintColor: UIColor = UIColor(rgb: 0x5085CB) /// Navigation Bar title color - public var navigationTitleColor: UIColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0xDE/255.0) + open var navigationTitleColor: UIColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0xDE/255.0) /// Navigation Bar subtitle color, default is 0.8 alhpa of navigationTitleColor - public var navigationSubtitleColor: UIColor { + open var navigationSubtitleColor: UIColor { get { return _navigationSubtitleColor != nil ? _navigationSubtitleColor! : navigationTitleColor.alpha(0.8) } set(v) { _navigationSubtitleColor = v } } - private var _navigationSubtitleColor: UIColor? + fileprivate var _navigationSubtitleColor: UIColor? /// Navigation Bar actove subtitle color, default is navigationTitleColor - public var navigationSubtitleActiveColor: UIColor { + open var navigationSubtitleActiveColor: UIColor { get { return _navigationSubtitleActiveColor != nil ? _navigationSubtitleActiveColor! : navigationTitleColor } set(v) { _navigationSubtitleActiveColor = v } } - private var _navigationSubtitleActiveColor: UIColor? + fileprivate var _navigationSubtitleActiveColor: UIColor? // // Token Field. Used at entering members of new group. // /// Token Text Color. Default is vcTextColor. - public var vcTokenFieldTextColor: UIColor { + open var vcTokenFieldTextColor: UIColor { get { return _vcTokenFieldTextColor != nil ? _vcTokenFieldTextColor! : vcTextColor } set(v) { _vcTokenFieldTextColor = v } } - private var _vcTokenFieldTextColor: UIColor? + fileprivate var _vcTokenFieldTextColor: UIColor? /// Background Color of Token field. Default is vcBgColor. - public var vcTokenFieldBgColor: UIColor { + open var vcTokenFieldBgColor: UIColor { get { return _vcTokenFieldBgColor != nil ? _vcTokenFieldBgColor! : vcBgColor } set(v) { _vcTokenFieldBgColor = v } } - private var _vcTokenFieldBgColor: UIColor? + fileprivate var _vcTokenFieldBgColor: UIColor? /// Token Tint Color. Default is vcTintColor. - public var vcTokenTintColor: UIColor { + open var vcTokenTintColor: UIColor { get { return _vcTokenTintColor != nil ? _vcTokenTintColor! : vcTintColor } set(v) { _vcTokenTintColor = v } } - private var _vcTokenTintColor: UIColor? + fileprivate var _vcTokenTintColor: UIColor? // // Search style // /// Style of status bar when search is active. - public var searchStatusBarStyle = UIStatusBarStyle.Default + open var searchStatusBarStyle = UIStatusBarStyle.default /// Background Color of search bar - public var searchBackgroundColor: UIColor { - get { return _searchBackgroundColor != nil ? _searchBackgroundColor! : UIColor.whiteColor() } + open var searchBackgroundColor: UIColor { + get { return _searchBackgroundColor != nil ? _searchBackgroundColor! : UIColor.white } set(v) { _searchBackgroundColor = v } } - private var _searchBackgroundColor: UIColor? + fileprivate var _searchBackgroundColor: UIColor? /// Cancel button color - public var searchCancelColor: UIColor { + open var searchCancelColor: UIColor { get { return _searchCancelColor != nil ? _searchCancelColor! : vcTintColor } set(v) { _searchCancelColor = v } } - private var _searchCancelColor: UIColor? + fileprivate var _searchCancelColor: UIColor? /// Search Input Field background color - public var searchFieldBgColor = UIColor(rgb: 0xededed) + open var searchFieldBgColor = UIColor(rgb: 0xededed) /// Search Input Field text color - public var searchFieldTextColor = UIColor.blackColor().alpha(0.56) + open var searchFieldTextColor = UIColor.black.alpha(0.56) // // UITabBarView style // /// Selected Text Color of UITabViewItem. Default is vcTintColor. - public var tabSelectedTextColor: UIColor { + open var tabSelectedTextColor: UIColor { get { return _tabSelectedTextColor != nil ? _tabSelectedTextColor! : vcTintColor } set(v) { _tabSelectedTextColor = v } } - private var _tabSelectedTextColor: UIColor? + fileprivate var _tabSelectedTextColor: UIColor? /// Selected Icon Color of UITableViewItem. Default is vcTintColor. - public var tabSelectedIconColor: UIColor { + open var tabSelectedIconColor: UIColor { get { return _tabSelectedIconColor != nil ? _tabSelectedIconColor! : vcTintColor } set(v) { _tabSelectedIconColor = v } } - private var _tabSelectedIconColor: UIColor? + fileprivate var _tabSelectedIconColor: UIColor? /// Unselected Text Color of UITabViewItem. Default is vcHintColor. - public var tabUnselectedTextColor: UIColor { + open var tabUnselectedTextColor: UIColor { get { return _tabUnselectedTextColor != nil ? _tabUnselectedTextColor! : vcHintColor } set(v) { _tabUnselectedTextColor = v } } - private var _tabUnselectedTextColor: UIColor? + fileprivate var _tabUnselectedTextColor: UIColor? /// Unselected Icon Color of UITableViewItem. Default is vcHintColor. - private var _tabUnselectedIconColor: UIColor? - public var tabUnselectedIconColor: UIColor { + fileprivate var _tabUnselectedIconColor: UIColor? + open var tabUnselectedIconColor: UIColor { get { return _tabUnselectedIconColor != nil ? _tabUnselectedIconColor! : vcHintColor } set(v) { _tabUnselectedIconColor = v } } /// Background color of UITabBarView. Default is vcPanelBgColor. - private var _tabBgColor: UIColor? - public var tabBgColor: UIColor { + fileprivate var _tabBgColor: UIColor? + open var tabBgColor: UIColor { get { return _tabBgColor != nil ? _tabBgColor! : vcPanelBgColor } set(v) { _tabBgColor = v } } @@ -155,106 +155,106 @@ public class ActorStyle { // /// Cell Background color. Default is vcBgColor. - public var cellBgColor: UIColor { + open var cellBgColor: UIColor { get { return _cellBgColor != nil ? _cellBgColor! : vcBgColor } set(v) { _cellBgColor = v } } - private var _cellBgColor: UIColor? + fileprivate var _cellBgColor: UIColor? /// Cell Background selected color. Default is vcSelectedColor. - public var cellBgSelectedColor: UIColor { + open var cellBgSelectedColor: UIColor { get { return _cellBgSelectedColor != nil ? _cellBgSelectedColor! : vcSelectedColor } set(v) { _cellBgSelectedColor = v } } - private var _cellBgSelectedColor: UIColor? + fileprivate var _cellBgSelectedColor: UIColor? /// Cell text color. Default is vcTextColor. - public var cellTextColor: UIColor { + open var cellTextColor: UIColor { get { return _cellTextColor != nil ? _cellTextColor! : vcTextColor } set(v) { _cellTextColor = v } } - private var _cellTextColor: UIColor? + fileprivate var _cellTextColor: UIColor? /// Cell hint text color. Default is vcHintColor. - public var cellHintColor: UIColor { + open var cellHintColor: UIColor { get { return _cellHintColor != nil ? _cellHintColor! : vcHintColor } set(v) { _cellHintColor = v } } - private var _cellHintColor: UIColor? + fileprivate var _cellHintColor: UIColor? /// Cell action color. Default is vcTintColor. - public var cellTintColor: UIColor { + open var cellTintColor: UIColor { get { return _cellTintColor != nil ? _cellTintColor! : vcTintColor } set(v) { _cellTintColor = v } } - private var _cellTintColor: UIColor? + fileprivate var _cellTintColor: UIColor? /// Cell desctructive color. Default is vcDestructiveColor. - public var cellDestructiveColor: UIColor { + open var cellDestructiveColor: UIColor { get { return _cellDestructiveColor != nil ? _cellDestructiveColor! : vcDestructiveColor } set(v) { _cellDestructiveColor = v } } - private var _cellDestructiveColor: UIColor? + fileprivate var _cellDestructiveColor: UIColor? /// Section header color. Default is vcSectionColor. - public var cellHeaderColor: UIColor { + open var cellHeaderColor: UIColor { get { return _cellHeaderColor != nil ? _cellHeaderColor! : vcSectionColor } set(v) { _cellHeaderColor = v } } - private var _cellHeaderColor: UIColor? + fileprivate var _cellHeaderColor: UIColor? /// Section footer color. Default is vcSectionColor. - public var cellFooterColor: UIColor { + open var cellFooterColor: UIColor { get { return _cellFooterColor != nil ? _cellFooterColor! : vcSectionColor } set(v) { _cellFooterColor = v } } - private var _cellFooterColor: UIColor? + fileprivate var _cellFooterColor: UIColor? // // Full screen placeholder style // /// Big Placeholder background color - public var placeholderBgColor: UIColor { + open var placeholderBgColor: UIColor { get { return _placeholderBgColor != nil ? _placeholderBgColor! : navigationBgColor.fromTransparentBar() } set(v) { _placeholderBgColor = v } } - private var _placeholderBgColor: UIColor? + fileprivate var _placeholderBgColor: UIColor? /// Big placeholder title color - public var placeholderTitleColor: UIColor { + open var placeholderTitleColor: UIColor { get { return _placeholderTitleColor != nil ? _placeholderTitleColor! : vcTextColor } set(v) { _placeholderTitleColor = v } } - private var _placeholderTitleColor: UIColor? + fileprivate var _placeholderTitleColor: UIColor? /// Bit Placeholder hint color - public var placeholderHintColor: UIColor { + open var placeholderHintColor: UIColor { get { return _placeholderHintColor != nil ? _placeholderHintColor! : vcHintColor } set(v) { _placeholderHintColor = v } } - private var _placeholderHintColor: UIColor? + fileprivate var _placeholderHintColor: UIColor? // // Avatar Placeholder and name colors // - public var avatarTextColor = UIColor.whiteColor() + open var avatarTextColor = UIColor.white - public var avatarLightBlue = UIColor(rgb: 0x59b7d3) - public var nameLightBlue = UIColor(rgb: 0x59b7d3) + open var avatarLightBlue = UIColor(rgb: 0x59b7d3) + open var nameLightBlue = UIColor(rgb: 0x59b7d3) - public var avatarDarkBlue = UIColor(rgb: 0x1d4e6f) - public var nameDarkBlue = UIColor(rgb: 0x1d4e6f) + open var avatarDarkBlue = UIColor(rgb: 0x1d4e6f) + open var nameDarkBlue = UIColor(rgb: 0x1d4e6f) - public var avatarPurple = UIColor(rgb: 0x995794) - public var namePurple = UIColor(rgb: 0x995794) + open var avatarPurple = UIColor(rgb: 0x995794) + open var namePurple = UIColor(rgb: 0x995794) - public var avatarPink = UIColor(rgb: 0xff506c) - public var namePink = UIColor(rgb: 0xff506c) + open var avatarPink = UIColor(rgb: 0xff506c) + open var namePink = UIColor(rgb: 0xff506c) - public var avatarOrange = UIColor(rgb: 0xf99341) - public var nameOrange = UIColor(rgb: 0xf99341) + open var avatarOrange = UIColor(rgb: 0xf99341) + open var nameOrange = UIColor(rgb: 0xf99341) - public var avatarYellow = UIColor(rgb: 0xe4d027) - public var nameYellow = UIColor(rgb: 0xe4d027) + open var avatarYellow = UIColor(rgb: 0xe4d027) + open var nameYellow = UIColor(rgb: 0xe4d027) - public var avatarGreen = UIColor(rgb: 0xe4d027) - public var nameGreen = UIColor(rgb: 0xe4d027) + open var avatarGreen = UIColor(rgb: 0xe4d027) + open var nameGreen = UIColor(rgb: 0xe4d027) - private var _avatarColors: [UIColor]? - public var avatarColors: [UIColor] { + fileprivate var _avatarColors: [UIColor]? + open var avatarColors: [UIColor] { get { if _avatarColors == nil { return [ @@ -273,8 +273,8 @@ public class ActorStyle { set(v) { _avatarColors = v } } - private var _nameColors: [UIColor]? - public var nameColors: [UIColor] { + fileprivate var _nameColors: [UIColor]? + open var nameColors: [UIColor] { get { if _nameColors == nil { return [ @@ -299,329 +299,329 @@ public class ActorStyle { // Text colors - private var _chatTextColor: UIColor? - public var chatTextColor: UIColor { + fileprivate var _chatTextColor: UIColor? + open var chatTextColor: UIColor { get { return _chatTextColor != nil ? _chatTextColor! : vcTextColor } set(v) { _chatTextColor = v } } - private var _chatUrlColor: UIColor? - public var chatUrlColor: UIColor { + fileprivate var _chatUrlColor: UIColor? + open var chatUrlColor: UIColor { get { return _chatUrlColor != nil ? _chatUrlColor! : vcTintColor } set(v) { _chatUrlColor = v } } - private var _chatTextUnsupportedColor: UIColor? - public var chatTextUnsupportedColor: UIColor { + fileprivate var _chatTextUnsupportedColor: UIColor? + open var chatTextUnsupportedColor: UIColor { get { return _chatTextUnsupportedColor != nil ? _chatTextUnsupportedColor! : vcTintColor.alpha(0.54) } set(v) { _chatTextUnsupportedColor = v } } - private var _chatTextOutColor: UIColor? - public var chatTextOutColor: UIColor { + fileprivate var _chatTextOutColor: UIColor? + open var chatTextOutColor: UIColor { get { return _chatTextOutColor != nil ? _chatTextOutColor! : chatTextColor } set(v) { _chatTextOutColor = v } } - private var _chatTextInColor: UIColor? - public var chatTextInColor: UIColor { + fileprivate var _chatTextInColor: UIColor? + open var chatTextInColor: UIColor { get { return _chatTextInColor != nil ? _chatTextInColor! : chatTextColor } set(v) { _chatTextInColor = v } } - private var _chatTextOutUnsupportedColor: UIColor? - public var chatTextOutUnsupportedColor: UIColor { + fileprivate var _chatTextOutUnsupportedColor: UIColor? + open var chatTextOutUnsupportedColor: UIColor { get { return _chatTextOutUnsupportedColor != nil ? _chatTextOutUnsupportedColor! : chatTextUnsupportedColor } set(v) { _chatTextOutUnsupportedColor = v } } - private var _chatTextInUnsupportedColor: UIColor? - public var chatTextInUnsupportedColor: UIColor { + fileprivate var _chatTextInUnsupportedColor: UIColor? + open var chatTextInUnsupportedColor: UIColor { get { return _chatTextInUnsupportedColor != nil ? _chatTextInUnsupportedColor! : chatTextUnsupportedColor } set(v) { _chatTextInUnsupportedColor = v } } - public var chatDateTextColor = UIColor.whiteColor() + open var chatDateTextColor = UIColor.white - public var chatServiceTextColor = UIColor.whiteColor() + open var chatServiceTextColor = UIColor.white - public var chatUnreadTextColor = UIColor.whiteColor() + open var chatUnreadTextColor = UIColor.white // Date colors - public var chatTextDateOutColor = UIColor.alphaBlack(0.27) - public var chatTextDateInColor = UIColor(rgb: 0x979797) + open var chatTextDateOutColor = UIColor.alphaBlack(0.27) + open var chatTextDateInColor = UIColor(rgb: 0x979797) - public var chatMediaDateColor = UIColor.whiteColor() - public var chatMediaDateBgColor = UIColor.blackColor().alpha(0.4) + open var chatMediaDateColor = UIColor.white + open var chatMediaDateBgColor = UIColor.black.alpha(0.4) // Bubble Colors - public var chatTextBubbleOutColor = UIColor(rgb: 0xD2FEFD) + open var chatTextBubbleOutColor = UIColor(rgb: 0xD2FEFD) - public var chatTextBubbleOutSelectedColor = UIColor.lightGrayColor() + open var chatTextBubbleOutSelectedColor = UIColor.lightGray - public var chatTextBubbleOutBorderColor = UIColor(rgb: 0x99E4E3) + open var chatTextBubbleOutBorderColor = UIColor(rgb: 0x99E4E3) - public var chatTextBubbleInColor = UIColor.whiteColor() + open var chatTextBubbleInColor = UIColor.white - public var chatTextBubbleInSelectedColor = UIColor.blueColor() + open var chatTextBubbleInSelectedColor = UIColor.blue - public var chatTextBubbleInBorderColor = UIColor(rgb: 0xCCCCCC) + open var chatTextBubbleInBorderColor = UIColor(rgb: 0xCCCCCC) - public var chatMediaBubbleColor = UIColor.whiteColor() - public var chatMediaBubbleBorderColor = UIColor(rgb: 0xCCCCCC) + open var chatMediaBubbleColor = UIColor.white + open var chatMediaBubbleBorderColor = UIColor(rgb: 0xCCCCCC) - public var chatDateBubbleColor = UIColor(rgb: 0x2D394A, alpha: 0.56) + open var chatDateBubbleColor = UIColor(rgb: 0x2D394A, alpha: 0.56) - public var chatServiceBubbleColor = UIColor(rgb: 0x2D394A, alpha: 0.56) + open var chatServiceBubbleColor = UIColor(rgb: 0x2D394A, alpha: 0.56) - public var chatUnreadBgColor = UIColor.alphaBlack(0.3) + open var chatUnreadBgColor = UIColor.alphaBlack(0.3) - public var chatReadMediaColor = UIColor(red: 46.6/255.0, green: 211.3/255.0, blue: 253.6/255.0, alpha: 1.0) + open var chatReadMediaColor = UIColor(red: 46.6/255.0, green: 211.3/255.0, blue: 253.6/255.0, alpha: 1.0) // Bubble Shadow - public var bubbleShadowEnabled = false + open var bubbleShadowEnabled = false - public var chatTextBubbleShadowColor = UIColor.alphaBlack(0.1) + open var chatTextBubbleShadowColor = UIColor.alphaBlack(0.1) // Status Colors - public lazy var chatIconCheck1 = UIImage.templated("msg_check_1") - public lazy var chatIconCheck2 = UIImage.templated("msg_check_2") - public lazy var chatIconError = UIImage.templated("msg_error") - public lazy var chatIconWarring = UIImage.templated("msg_warring") - public lazy var chatIconClock = UIImage.templated("msg_clock") + open lazy var chatIconCheck1 = UIImage.templated("msg_check_1") + open lazy var chatIconCheck2 = UIImage.templated("msg_check_2") + open lazy var chatIconError = UIImage.templated("msg_error") + open lazy var chatIconWarring = UIImage.templated("msg_warring") + open lazy var chatIconClock = UIImage.templated("msg_clock") - private var _chatStatusActive: UIColor? - public var chatStatusActive: UIColor { + fileprivate var _chatStatusActive: UIColor? + open var chatStatusActive: UIColor { get { return _chatStatusActive != nil ? _chatStatusActive! : vcTintColor } set(v) { _chatStatusActive = v } } - private var _chatStatusPassive: UIColor? - public var chatStatusPassive: UIColor { + fileprivate var _chatStatusPassive: UIColor? + open var chatStatusPassive: UIColor { get { return _chatStatusPassive != nil ? _chatStatusPassive! : vcHintColor } set(v) { _chatStatusPassive = v } } - private var _chatStatusDanger: UIColor? - public var chatStatusDanger: UIColor { + fileprivate var _chatStatusDanger: UIColor? + open var chatStatusDanger: UIColor { get { return _chatStatusDanger != nil ? _chatStatusDanger! : vcDestructiveColor } set(v) { _chatStatusDanger = v } } - private var _chatStatusMediaActive: UIColor? - public var chatStatusMediaActive: UIColor { + fileprivate var _chatStatusMediaActive: UIColor? + open var chatStatusMediaActive: UIColor { get { return _chatStatusMediaActive != nil ? _chatStatusMediaActive! : chatReadMediaColor } set(v) { _chatStatusMediaActive = v } } - private var _chatStatusMediaPassive: UIColor? - public var chatStatusMediaPassive: UIColor { - get { return _chatStatusMediaPassive != nil ? _chatStatusMediaPassive! : UIColor.whiteColor() } + fileprivate var _chatStatusMediaPassive: UIColor? + open var chatStatusMediaPassive: UIColor { + get { return _chatStatusMediaPassive != nil ? _chatStatusMediaPassive! : UIColor.white } set(v) { _chatStatusMediaPassive = v } } - private var _chatStatusMediaDanger: UIColor? - public var chatStatusMediaDanger: UIColor { + fileprivate var _chatStatusMediaDanger: UIColor? + open var chatStatusMediaDanger: UIColor { get { return _chatStatusMediaDanger != nil ? _chatStatusMediaDanger! : chatStatusDanger } set(v) { _chatStatusMediaDanger = v } } - private var _chatStatusSending: UIColor? - public var chatStatusSending: UIColor { + fileprivate var _chatStatusSending: UIColor? + open var chatStatusSending: UIColor { get { return _chatStatusSending != nil ? _chatStatusSending! : chatStatusPassive } set(v) { _chatStatusSending = v } } - private var _chatStatusSent: UIColor? - public var chatStatusSent: UIColor { + fileprivate var _chatStatusSent: UIColor? + open var chatStatusSent: UIColor { get { return _chatStatusSent != nil ? _chatStatusSent! : chatStatusPassive } set(v) { _chatStatusSent = v } } - private var _chatStatusReceived: UIColor? - public var chatStatusReceived: UIColor { + fileprivate var _chatStatusReceived: UIColor? + open var chatStatusReceived: UIColor { get { return _chatStatusReceived != nil ? _chatStatusReceived! : chatStatusPassive } set(v) { _chatStatusReceived = v } } - private var _chatStatusRead: UIColor? - public var chatStatusRead: UIColor { + fileprivate var _chatStatusRead: UIColor? + open var chatStatusRead: UIColor { get { return _chatStatusRead != nil ? _chatStatusRead! : chatStatusActive } set(v) { _chatStatusRead = v } } - private var _chatStatusError: UIColor? - public var chatStatusError: UIColor { + fileprivate var _chatStatusError: UIColor? + open var chatStatusError: UIColor { get { return _chatStatusError != nil ? _chatStatusError! : chatStatusDanger } set(v) { _chatStatusError = v } } - private var _chatStatusMediaSending: UIColor? - public var chatStatusMediaSending: UIColor { + fileprivate var _chatStatusMediaSending: UIColor? + open var chatStatusMediaSending: UIColor { get { return _chatStatusMediaSending != nil ? _chatStatusMediaSending! : chatStatusMediaPassive } set(v) { _chatStatusMediaSending = v } } - private var _chatStatusMediaSent: UIColor? - public var chatStatusMediaSent: UIColor { + fileprivate var _chatStatusMediaSent: UIColor? + open var chatStatusMediaSent: UIColor { get { return _chatStatusMediaSent != nil ? _chatStatusMediaSent! : chatStatusMediaPassive } set(v) { _chatStatusMediaSent = v } } - private var _chatStatusMediaReceived: UIColor? - public var chatStatusMediaReceived: UIColor { + fileprivate var _chatStatusMediaReceived: UIColor? + open var chatStatusMediaReceived: UIColor { get { return _chatStatusMediaReceived != nil ? _chatStatusMediaReceived! : chatStatusMediaPassive } set(v) { _chatStatusMediaReceived = v } } - private var _chatStatusMediaRead: UIColor? - public var chatStatusMediaRead: UIColor { + fileprivate var _chatStatusMediaRead: UIColor? + open var chatStatusMediaRead: UIColor { get { return _chatStatusMediaRead != nil ? _chatStatusMediaRead! : chatStatusMediaActive } set(v) { _chatStatusMediaRead = v } } - private var _chatStatusMediaError: UIColor? - public var chatStatusMediaError: UIColor { + fileprivate var _chatStatusMediaError: UIColor? + open var chatStatusMediaError: UIColor { get { return _chatStatusMediaError != nil ? _chatStatusMediaError! : chatStatusMediaDanger } set(v) { _chatStatusMediaError = v } } // Chat screen - private var _chatInputField: UIColor? - public var chatInputFieldBgColor: UIColor { + fileprivate var _chatInputField: UIColor? + open var chatInputFieldBgColor: UIColor { get { return _chatInputField != nil ? _chatInputField! : vcPanelBgColor } set(v) { _chatInputField = v } } - private var _chatAttachColor: UIColor? - public var chatAttachColor: UIColor { + fileprivate var _chatAttachColor: UIColor? + open var chatAttachColor: UIColor { get { return _chatAttachColor != nil ? _chatAttachColor! : vcTintColor } set(v) { _chatAttachColor = v } } - private var _chatSendColor: UIColor? - public var chatSendColor: UIColor { + fileprivate var _chatSendColor: UIColor? + open var chatSendColor: UIColor { get { return _chatSendColor != nil ? _chatSendColor! : vcTintColor } set(v) { _chatSendColor = v } } - private var _chatSendDisabledColor: UIColor? - public var chatSendDisabledColor: UIColor { + fileprivate var _chatSendDisabledColor: UIColor? + open var chatSendDisabledColor: UIColor { get { return _chatSendDisabledColor != nil ? _chatSendDisabledColor! : vcTintColor.alpha(0.64) } set(v) { _chatSendDisabledColor = v } } - private var _chatAutocompleteHighlight: UIColor? - public var chatAutocompleteHighlight: UIColor { + fileprivate var _chatAutocompleteHighlight: UIColor? + open var chatAutocompleteHighlight: UIColor { get { return _chatAutocompleteHighlight != nil ? _chatAutocompleteHighlight! : vcTintColor } set(v) { _chatAutocompleteHighlight = v } } - public lazy var chatBgColor = UIColor(patternImage: UIImage.bundled("chat_bg")!) + open lazy var chatBgColor = UIColor(patternImage: UIImage.bundled("chat_bg")!) // // Dialogs styles // - private var _dialogTitleColor: UIColor? - public var dialogTitleColor: UIColor { + fileprivate var _dialogTitleColor: UIColor? + open var dialogTitleColor: UIColor { get { return _dialogTitleColor != nil ? _dialogTitleColor! : vcTextColor } set(v) { _dialogTitleColor = v } } - private var _dialogTextColor: UIColor? - public var dialogTextColor: UIColor { + fileprivate var _dialogTextColor: UIColor? + open var dialogTextColor: UIColor { get { return _dialogTextColor != nil ? _dialogTextColor! : dialogTitleColor.alpha(0.64) } set(v) { _dialogTextColor = v } } - private var _dialogTextActiveColor: UIColor? - public var dialogTextActiveColor: UIColor { + fileprivate var _dialogTextActiveColor: UIColor? + open var dialogTextActiveColor: UIColor { get { return _dialogTextActiveColor != nil ? _dialogTextActiveColor! : vcTextColor } set(v) { _dialogTextActiveColor = v } } - private var _dialogDateColor: UIColor? - public var dialogDateColor: UIColor { + fileprivate var _dialogDateColor: UIColor? + open var dialogDateColor: UIColor { get { return _dialogDateColor != nil ? _dialogDateColor! : vcHintColor } set(v) { _dialogDateColor = v } } - public var dialogCounterBgColor: UIColor = UIColor(rgb: 0x50A1D6) + open var dialogCounterBgColor: UIColor = UIColor(rgb: 0x50A1D6) - public var dialogCounterColor: UIColor = UIColor.whiteColor() + open var dialogCounterColor: UIColor = UIColor.white - private var _dialogStatusActive: UIColor? - public var dialogStatusActive: UIColor { + fileprivate var _dialogStatusActive: UIColor? + open var dialogStatusActive: UIColor { get { return _dialogStatusActive != nil ? _dialogStatusActive! : chatStatusActive } set(v) { _dialogStatusActive = v } } - private var _dialogStatusPassive: UIColor? - public var dialogStatusPassive: UIColor { + fileprivate var _dialogStatusPassive: UIColor? + open var dialogStatusPassive: UIColor { get { return _dialogStatusPassive != nil ? _dialogStatusPassive! : chatStatusPassive } set(v) { _dialogStatusPassive = v } } - private var _dialogStatusDanger: UIColor? - public var dialogStatusDanger: UIColor { + fileprivate var _dialogStatusDanger: UIColor? + open var dialogStatusDanger: UIColor { get { return _dialogStatusDanger != nil ? _dialogStatusDanger! : chatStatusDanger } set(v) { _dialogStatusDanger = v } } - private var _dialogStatusSending: UIColor? - public var dialogStatusSending: UIColor { + fileprivate var _dialogStatusSending: UIColor? + open var dialogStatusSending: UIColor { get { return _dialogStatusSending != nil ? _dialogStatusSending! : dialogStatusPassive } set(v) { _dialogStatusSending = v } } - private var _dialogStatusSent: UIColor? - public var dialogStatusSent: UIColor { + fileprivate var _dialogStatusSent: UIColor? + open var dialogStatusSent: UIColor { get { return _dialogStatusSent != nil ? _dialogStatusSent! : dialogStatusPassive } set(v) { _dialogStatusSent = v } } - private var _dialogStatusReceived: UIColor? - public var dialogStatusReceived: UIColor { + fileprivate var _dialogStatusReceived: UIColor? + open var dialogStatusReceived: UIColor { get { return _dialogStatusReceived != nil ? _dialogStatusReceived! : dialogStatusPassive } set(v) { _dialogStatusReceived = v } } - private var _dialogStatusRead: UIColor? - public var dialogStatusRead: UIColor { + fileprivate var _dialogStatusRead: UIColor? + open var dialogStatusRead: UIColor { get { return _dialogStatusRead != nil ? _dialogStatusRead! : dialogStatusActive } set(v) { _dialogStatusRead = v } } - private var _dialogStatusError: UIColor? - public var dialogStatusError: UIColor { + fileprivate var _dialogStatusError: UIColor? + open var dialogStatusError: UIColor { get { return _dialogStatusError != nil ? _dialogStatusError! : dialogStatusDanger } set(v) { _dialogStatusError = v } } - public var dialogAvatarSize: CGFloat = 50 + open var dialogAvatarSize: CGFloat = 50 - private var _statusBackgroundIcon: UIImage? - public var statusBackgroundImage:UIImage { + fileprivate var _statusBackgroundIcon: UIImage? + open var statusBackgroundImage:UIImage { get { if (_statusBackgroundIcon == nil){ - let statusImage:UIImage = UIImage.bundled("bubble_service_bg")!.aa_imageWithColor(UIColor.blackColor().colorWithAlphaComponent(0.7)).imageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) + let statusImage:UIImage = UIImage.bundled("bubble_service_bg")!.aa_imageWithColor(UIColor.black.withAlphaComponent(0.7)).withRenderingMode(UIImageRenderingMode.alwaysOriginal) - let center:CGPoint = CGPointMake(statusImage.size.width / 2.0, statusImage.size.height / 2.0); + let center:CGPoint = CGPoint(x: statusImage.size.width / 2.0, y: statusImage.size.height / 2.0); let capInsets:UIEdgeInsets = UIEdgeInsetsMake(center.y, center.x, center.y, center.x); - _statusBackgroundIcon = statusImage.resizableImageWithCapInsets(capInsets, resizingMode: UIImageResizingMode.Stretch) + _statusBackgroundIcon = statusImage.resizableImage(withCapInsets: capInsets, resizingMode: UIImageResizingMode.stretch) return _statusBackgroundIcon! } else { return _statusBackgroundIcon! @@ -633,8 +633,8 @@ public class ActorStyle { // Contacts styles // - private var _contactTitleColor: UIColor? - public var contactTitleColor: UIColor { + fileprivate var _contactTitleColor: UIColor? + open var contactTitleColor: UIColor { get { return _contactTitleColor != nil ? _contactTitleColor! : vcTextColor } set(v) { _contactTitleColor = v } } @@ -643,26 +643,26 @@ public class ActorStyle { // Online styles // - private var _userOnlineColor: UIColor? - public var userOnlineColor: UIColor { + fileprivate var _userOnlineColor: UIColor? + open var userOnlineColor: UIColor { get { return _userOnlineColor != nil ? _userOnlineColor! : vcTintColor } set(v) { _userOnlineColor = v } } - private var _userOfflineColor: UIColor? - public var userOfflineColor: UIColor { + fileprivate var _userOfflineColor: UIColor? + open var userOfflineColor: UIColor { get { return _userOfflineColor != nil ? _userOfflineColor! : vcTextColor.alpha(0.54) } set(v) { _userOfflineColor = v } } - private var _userOnlineNavigationColor: UIColor? - public var userOnlineNavigationColor: UIColor { + fileprivate var _userOnlineNavigationColor: UIColor? + open var userOnlineNavigationColor: UIColor { get { return _userOnlineNavigationColor != nil ? _userOnlineNavigationColor! : userOnlineColor } set(v) { _userOnlineNavigationColor = v } } - private var _userOfflineNavigationColor: UIColor? - public var userOfflineNavigationColor: UIColor { + fileprivate var _userOfflineNavigationColor: UIColor? + open var userOfflineNavigationColor: UIColor { get { return _userOfflineNavigationColor != nil ? _userOfflineNavigationColor! : navigationSubtitleColor } set(v) { _userOfflineNavigationColor = v } } @@ -671,20 +671,20 @@ public class ActorStyle { // Compose styles // - private var _composeAvatarBgColor: UIColor? - public var composeAvatarBgColor: UIColor { + fileprivate var _composeAvatarBgColor: UIColor? + open var composeAvatarBgColor: UIColor { get { return _composeAvatarBgColor != nil ? _composeAvatarBgColor! : vcBgColor } set(v) { _composeAvatarBgColor = v } } - private var _composeAvatarBorderColor: UIColor? - public var composeAvatarBorderColor: UIColor { + fileprivate var _composeAvatarBorderColor: UIColor? + open var composeAvatarBorderColor: UIColor { get { return _composeAvatarBorderColor != nil ? _composeAvatarBorderColor! : vcSeparatorColor } set(v) { _composeAvatarBorderColor = v } } - private var _composeAvatarTextColor: UIColor? - public var composeAvatarTextColor: UIColor { + fileprivate var _composeAvatarTextColor: UIColor? + open var composeAvatarTextColor: UIColor { get { return _composeAvatarTextColor != nil ? _composeAvatarTextColor! : vcHintColor } set(v) { _composeAvatarTextColor = v } } @@ -695,18 +695,18 @@ public class ActorStyle { // /// Is Status Bar connecting status hidden - public var statusBarConnectingHidden = false + open var statusBarConnectingHidden = false /// Is Status Bar background color - private var _statusBarConnectingBgColor : UIColor? - public var statusBarConnectingBgColor: UIColor { + fileprivate var _statusBarConnectingBgColor : UIColor? + open var statusBarConnectingBgColor: UIColor { get { return _statusBarConnectingBgColor != nil ? _statusBarConnectingBgColor! : navigationBgColor } set(v) { _statusBarConnectingBgColor = v } } /// Is Status Bar background color - private var _statusBarConnectingTextColor : UIColor? - public var statusBarConnectingTextColor: UIColor { + fileprivate var _statusBarConnectingTextColor : UIColor? + open var statusBarConnectingTextColor: UIColor { get { return _statusBarConnectingTextColor != nil ? _statusBarConnectingTextColor! : navigationTitleColor } set(v) { _statusBarConnectingTextColor = v } } @@ -716,54 +716,54 @@ public class ActorStyle { // /// Welcome Page Background color - public var welcomeBgColor = UIColor(red: 94, green: 142, blue: 192) + open var welcomeBgColor = UIColor(red: 94, green: 142, blue: 192) /// Welcome Page Background image - public var welcomeBgImage: UIImage? = nil + open var welcomeBgImage: UIImage? = nil /// Welcome Page Title Color - public var welcomeTitleColor = UIColor.whiteColor() + open var welcomeTitleColor = UIColor.white /// Welcome Page Tagline Color - public var welcomeTaglineColor = UIColor.whiteColor() + open var welcomeTaglineColor = UIColor.white /// Welcome Page Signup Background Color - public var welcomeSignupBgColor = UIColor.whiteColor() + open var welcomeSignupBgColor = UIColor.white /// Welcome Page Signup Text Color - public var welcomeSignupTextColor = UIColor(red: 94, green: 142, blue: 192) + open var welcomeSignupTextColor = UIColor(red: 94, green: 142, blue: 192) /// Welcome Page Login Text Color - public var welcomeLoginTextColor = UIColor.whiteColor() + open var welcomeLoginTextColor = UIColor.white /// Welcome Logo - public var welcomeLogo: UIImage? = UIImage.bundled("logo_welcome") - public var welcomeLogoSize: CGSize = CGSize(width: 90, height: 90) - public var logoViewVerticalGap: CGFloat = 145 + open var welcomeLogo: UIImage? = UIImage.bundled("logo_welcome") + open var welcomeLogoSize: CGSize = CGSize(width: 90, height: 90) + open var logoViewVerticalGap: CGFloat = 145 // // Auth Screen // - public var authTintColor = UIColor(rgb: 0x007aff) + open var authTintColor = UIColor(rgb: 0x007aff) - public var authTitleColor = UIColor.blackColor().alpha(0.87) + open var authTitleColor = UIColor.black.alpha(0.87) - public var authHintColor = UIColor.alphaBlack(0.64) + open var authHintColor = UIColor.alphaBlack(0.64) - public var authTextColor = UIColor.alphaBlack(0.87) + open var authTextColor = UIColor.alphaBlack(0.87) - public var authSeparatorColor = UIColor.blackColor().alpha(0.2) + open var authSeparatorColor = UIColor.black.alpha(0.2) // // Settings VC // - public var vcSettingsContactsHeaderTextColor: UIColor { + open var vcSettingsContactsHeaderTextColor: UIColor { get { return _vcSettingsContactsHeaderTextColor != nil ? _vcSettingsContactsHeaderTextColor! : vcTextColor } set(v) { _vcSettingsContactsHeaderTextColor = v } } - private var _vcSettingsContactsHeaderTextColor : UIColor? + fileprivate var _vcSettingsContactsHeaderTextColor : UIColor? } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthEmailViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthEmailViewController.swift index 12e1a586e8..ce9dbfa99a 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthEmailViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthEmailViewController.swift @@ -4,7 +4,7 @@ import Foundation -public class AAAuthEmailViewController: AAAuthViewController { +open class AAAuthEmailViewController: AAAuthViewController { let name: String @@ -28,12 +28,12 @@ public class AAAuthEmailViewController: AAAuthViewController { fatalError("init(coder:) has not been implemented") } - public override func viewDidLoad() { + open override func viewDidLoad() { - view.backgroundColor = UIColor.whiteColor() + view.backgroundColor = UIColor.white - scrollView.keyboardDismissMode = .OnDrag - scrollView.scrollEnabled = true + scrollView.keyboardDismissMode = .onDrag + scrollView.isScrollEnabled = true scrollView.alwaysBounceVertical = true welcomeLabel.font = UIFont.lightSystemFontOfSize(23) @@ -42,20 +42,20 @@ public class AAAuthEmailViewController: AAAuthViewController { welcomeLabel.numberOfLines = 1 welcomeLabel.minimumScaleFactor = 0.3 welcomeLabel.adjustsFontSizeToFitWidth = true - welcomeLabel.textAlignment = .Center + welcomeLabel.textAlignment = .center - hintLabel.font = UIFont.systemFontOfSize(14) + hintLabel.font = UIFont.systemFont(ofSize: 14) hintLabel.textColor = ActorSDK.sharedActor().style.authHintColor hintLabel.text = AALocalized("AuthEmailHint") hintLabel.numberOfLines = 1 - hintLabel.textAlignment = .Center + hintLabel.textAlignment = .center - emailField.font = UIFont.systemFontOfSize(17) + emailField.font = UIFont.systemFont(ofSize: 17) emailField.textColor = ActorSDK.sharedActor().style.authTextColor emailField.placeholder = AALocalized("AuthEmailPlaceholder") - emailField.keyboardType = .EmailAddress - emailField.autocapitalizationType = .None - emailField.autocorrectionType = .No + emailField.keyboardType = .emailAddress + emailField.autocapitalizationType = .none + emailField.autocorrectionType = .no emailFieldLine.backgroundColor = ActorSDK.sharedActor().style.authSeparatorColor @@ -110,21 +110,21 @@ public class AAAuthEmailViewController: AAAuthViewController { } termsLabel.attributedText = attributedTerms - termsLabel.font = UIFont.systemFontOfSize(14) + termsLabel.font = UIFont.systemFont(ofSize: 14) termsLabel.numberOfLines = 2 - termsLabel.textAlignment = .Center + termsLabel.textAlignment = .center } else { - termsLabel.hidden = true + termsLabel.isHidden = true } - if ActorSDK.sharedActor().authStrategy == .PhoneOnly || ActorSDK.sharedActor().authStrategy == .PhoneEmail { - usePhoneButton.setTitle(AALocalized("AuthEmailUsePhone"), forState: .Normal) - usePhoneButton.titleLabel?.font = UIFont.systemFontOfSize(14) - usePhoneButton.setTitleColor(ActorSDK.sharedActor().style.authTintColor, forState: .Normal) - usePhoneButton.setTitleColor(ActorSDK.sharedActor().style.authTintColor.alpha(0.56), forState: .Highlighted) - usePhoneButton.addTarget(self, action: #selector(AAAuthEmailViewController.usePhoneDidPressed), forControlEvents: .TouchUpInside) + if ActorSDK.sharedActor().authStrategy == .phoneOnly || ActorSDK.sharedActor().authStrategy == .phoneEmail { + usePhoneButton.setTitle(AALocalized("AuthEmailUsePhone"), for: UIControlState()) + usePhoneButton.titleLabel?.font = UIFont.systemFont(ofSize: 14) + usePhoneButton.setTitleColor(ActorSDK.sharedActor().style.authTintColor, for: UIControlState()) + usePhoneButton.setTitleColor(ActorSDK.sharedActor().style.authTintColor.alpha(0.56), for: .highlighted) + usePhoneButton.addTarget(self, action: #selector(AAAuthEmailViewController.usePhoneDidPressed), for: .touchUpInside) } else { - usePhoneButton.hidden = true + usePhoneButton.isHidden = true } scrollView.addSubview(welcomeLabel) @@ -138,30 +138,30 @@ public class AAAuthEmailViewController: AAAuthViewController { super.viewDidLoad() } - public override func viewDidLayoutSubviews() { + open override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - welcomeLabel.frame = CGRectMake(20, 90 - 66, view.width - 40, 28) - hintLabel.frame = CGRectMake(20, 127 - 66, view.width - 40, 18) + welcomeLabel.frame = CGRect(x: 20, y: 90 - 66, width: view.width - 40, height: 28) + hintLabel.frame = CGRect(x: 20, y: 127 - 66, width: view.width - 40, height: 18) - emailField.frame = CGRectMake(20, 184 - 66, view.width - 40, 44) - emailFieldLine.frame = CGRectMake(10, 228 - 66, view.width - 20, 0.5) + emailField.frame = CGRect(x: 20, y: 184 - 66, width: view.width - 40, height: 44) + emailFieldLine.frame = CGRect(x: 10, y: 228 - 66, width: view.width - 20, height: 0.5) - termsLabel.frame = CGRectMake(20, 314 - 66, view.width - 40, 55) + termsLabel.frame = CGRect(x: 20, y: 314 - 66, width: view.width - 40, height: 55) - usePhoneButton.frame = CGRectMake(20, 375 - 66, view.width - 40, 38) + usePhoneButton.frame = CGRect(x: 20, y: 375 - 66, width: view.width - 40, height: 38) scrollView.frame = view.bounds - scrollView.contentSize = CGSizeMake(view.width, 420) + scrollView.contentSize = CGSize(width: view.width, height: 420) } - public func usePhoneDidPressed() { + open func usePhoneDidPressed() { let controllers = self.navigationController!.viewControllers let updatedControllers = Array(controllers[0..<(controllers.count - 1)]) + [AAAuthPhoneViewController(name: name)] self.navigationController?.setViewControllers(updatedControllers, animated: false) } - public override func nextDidTap() { + open override func nextDidTap() { let email = emailField.text! if !AATools.isValidEmail(email) { @@ -170,7 +170,7 @@ public class AAAuthEmailViewController: AAAuthViewController { return } - Actor.doStartAuthWithEmail(email).startUserAction().then { (res: ACAuthStartRes!) -> () in + Actor.doStartAuth(withEmail: email).startUserAction().then { (res: ACAuthStartRes!) -> () in if res.authMode.toNSEnum() == .OTP { self.navigateNext(AAAuthOTPViewController(email: email, name: self.name, transactionHash: res.transactionHash)) } else { @@ -179,8 +179,8 @@ public class AAAuthEmailViewController: AAAuthViewController { } } - public override func keyboardWillAppear(height: CGFloat) { - scrollView.frame = CGRectMake(0, 0, view.width, view.height - height) + open override func keyboardWillAppear(_ height: CGFloat) { + scrollView.frame = CGRect(x: 0, y: 0, width: view.width, height: view.height - height) if AADevice.isiPhone4 || AADevice.isiPhone5 { let height = scrollView.height - height @@ -191,13 +191,13 @@ public class AAAuthEmailViewController: AAAuthViewController { } } - public override func keyboardWillDisappear() { - scrollView.frame = CGRectMake(0, 0, view.width, view.height) + open override func keyboardWillDisappear() { + scrollView.frame = CGRect(x: 0, y: 0, width: view.width, height: view.height) } - public override func viewWillDisappear(animated: Bool) { + open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) emailField.resignFirstResponder() } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthLogInViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthLogInViewController.swift index a4dd70465a..0f9174ed45 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthLogInViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthLogInViewController.swift @@ -4,7 +4,7 @@ import Foundation -public class AAAuthLogInViewController: AAAuthViewController { +open class AAAuthLogInViewController: AAAuthViewController { let scrollView = UIScrollView() @@ -17,41 +17,41 @@ public class AAAuthLogInViewController: AAAuthViewController { public override init() { super.init(nibName: nil, bundle: nil) - navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: .Plain, target: self, action: #selector(AAViewController.dismiss)) + navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: .plain, target: self, action: #selector(AAViewController.dismissController)) } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public override func viewDidLoad() { + open override func viewDidLoad() { - view.backgroundColor = UIColor.whiteColor() + view.backgroundColor = UIColor.white - scrollView.keyboardDismissMode = .OnDrag - scrollView.scrollEnabled = true + scrollView.keyboardDismissMode = .onDrag + scrollView.isScrollEnabled = true scrollView.alwaysBounceVertical = true welcomeLabel.font = UIFont.lightSystemFontOfSize(23) welcomeLabel.text = AALocalized("AuthLoginTitle").replace("{app_name}", dest: ActorSDK.sharedActor().appName) welcomeLabel.textColor = ActorSDK.sharedActor().style.authTitleColor - welcomeLabel.textAlignment = .Center + welcomeLabel.textAlignment = .center - if ActorSDK.sharedActor().authStrategy == .PhoneOnly { + if ActorSDK.sharedActor().authStrategy == .phoneOnly { field.placeholder = AALocalized("AuthLoginPhone") - field.keyboardType = .PhonePad - } else if ActorSDK.sharedActor().authStrategy == .EmailOnly { + field.keyboardType = .phonePad + } else if ActorSDK.sharedActor().authStrategy == .emailOnly { field.placeholder = AALocalized("AuthLoginEmail") - field.keyboardType = .EmailAddress - } else if ActorSDK.sharedActor().authStrategy == .PhoneEmail { + field.keyboardType = .emailAddress + } else if ActorSDK.sharedActor().authStrategy == .phoneEmail { field.placeholder = AALocalized("AuthLoginPhoneEmail") - field.keyboardType = .Default + field.keyboardType = .default } - field.autocapitalizationType = .None - field.autocorrectionType = .No + field.autocapitalizationType = .none + field.autocorrectionType = .no fieldLine.backgroundColor = ActorSDK.sharedActor().style.authSeparatorColor - fieldLine.opaque = false + fieldLine.isOpaque = false scrollView.addSubview(welcomeLabel) scrollView.addSubview(field) @@ -61,19 +61,19 @@ public class AAAuthLogInViewController: AAAuthViewController { super.viewDidLoad() } - public override func viewDidLayoutSubviews() { + open override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - welcomeLabel.frame = CGRectMake(15, 90 - 66, view.width - 30, 28) + welcomeLabel.frame = CGRect(x: 15, y: 90 - 66, width: view.width - 30, height: 28) - fieldLine.frame = CGRectMake(10, 200 - 66, view.width - 20, 0.5) - field.frame = CGRectMake(20, 156 - 66, view.width - 40, 44) + fieldLine.frame = CGRect(x: 10, y: 200 - 66, width: view.width - 20, height: 0.5) + field.frame = CGRect(x: 20, y: 156 - 66, width: view.width - 40, height: 44) scrollView.frame = view.bounds - scrollView.contentSize = CGSizeMake(view.width, 240 - 66) + scrollView.contentSize = CGSize(width: view.width, height: 240 - 66) } - public override func nextDidTap() { + open override func nextDidTap() { let value = field.text!.trim() if value.length == 0 { shakeView(field, originalX: 20) @@ -81,9 +81,9 @@ public class AAAuthLogInViewController: AAAuthViewController { return } - if ActorSDK.sharedActor().authStrategy == .EmailOnly || ActorSDK.sharedActor().authStrategy == .PhoneEmail { + if ActorSDK.sharedActor().authStrategy == .emailOnly || ActorSDK.sharedActor().authStrategy == .phoneEmail { if (AATools.isValidEmail(value)) { - Actor.doStartAuthWithEmail(value).startUserAction().then { (res: ACAuthStartRes!) -> () in + Actor.doStartAuth(withEmail: value).startUserAction().then { (res: ACAuthStartRes!) -> () in if res.authMode.toNSEnum() == .OTP { self.navigateNext(AAAuthOTPViewController(email: value, transactionHash: res.transactionHash)) } else { @@ -94,13 +94,13 @@ public class AAAuthLogInViewController: AAAuthViewController { } } - if ActorSDK.sharedActor().authStrategy == .PhoneOnly || ActorSDK.sharedActor().authStrategy == .PhoneEmail { - let numbersSet = NSCharacterSet(charactersInString: "0123456789").invertedSet + if ActorSDK.sharedActor().authStrategy == .phoneOnly || ActorSDK.sharedActor().authStrategy == .phoneEmail { + let numbersSet = CharacterSet(charactersIn: "0123456789").inverted let stripped = value.strip(numbersSet) if let parsed = Int64(stripped) { - Actor.doStartAuthWithPhone(jlong(parsed)).startUserAction().then { (res: ACAuthStartRes!) -> () in + Actor.doStartAuth(withPhone: jlong(parsed)).startUserAction().then { (res: ACAuthStartRes!) -> () in if res.authMode.toNSEnum() == .OTP { - let formatted = RMPhoneFormat().format("\(parsed)") + let formatted = RMPhoneFormat().format("\(parsed)")! self.navigateNext(AAAuthOTPViewController(phone: formatted, transactionHash: res.transactionHash)) } else { self.alertUser(AALocalized("AuthUnsupported").replace("{app_name}", dest: ActorSDK.sharedActor().appName)) @@ -114,13 +114,13 @@ public class AAAuthLogInViewController: AAAuthViewController { shakeView(fieldLine, originalX: 10) } - public override func viewWillDisappear(animated: Bool) { + open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) field.resignFirstResponder() } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if isFirstAppear { @@ -128,4 +128,4 @@ public class AAAuthLogInViewController: AAAuthViewController { field.becomeFirstResponder() } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthNameViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthNameViewController.swift index 24e2e4608d..7943690112 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthNameViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthNameViewController.swift @@ -4,7 +4,7 @@ import Foundation -public class AAAuthNameViewController: AAAuthViewController { +open class AAAuthNameViewController: AAAuthViewController { let transactionHash: String? @@ -20,34 +20,34 @@ public class AAAuthNameViewController: AAAuthViewController { self.transactionHash = transactionHash super.init(nibName: nil, bundle: nil) - navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: .Plain, target: self, action: #selector(AAViewController.dismiss)) + navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: .plain, target: self, action: #selector(AAViewController.dismissController)) } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public override func viewDidLoad() { + open override func viewDidLoad() { - view.backgroundColor = UIColor.whiteColor() + view.backgroundColor = UIColor.white - scrollView.keyboardDismissMode = .OnDrag - scrollView.scrollEnabled = true + scrollView.keyboardDismissMode = .onDrag + scrollView.isScrollEnabled = true scrollView.alwaysBounceVertical = true welcomeLabel.font = UIFont.lightSystemFontOfSize(23) welcomeLabel.text = AALocalized("AuthNameTitle") welcomeLabel.textColor = ActorSDK.sharedActor().style.authTitleColor - welcomeLabel.textAlignment = .Center + welcomeLabel.textAlignment = .center field.placeholder = AALocalized("AuthNamePlaceholder") - field.keyboardType = .Default - field.autocapitalizationType = .Words + field.keyboardType = .default + field.autocapitalizationType = .words field.textColor = ActorSDK.sharedActor().style.authTextColor - field.addTarget(self, action: #selector(AAAuthNameViewController.fieldDidChanged), forControlEvents: .EditingChanged) + field.addTarget(self, action: #selector(AAAuthNameViewController.fieldDidChanged), for: .editingChanged) fieldLine.backgroundColor = ActorSDK.sharedActor().style.authSeparatorColor - fieldLine.opaque = false + fieldLine.isOpaque = false scrollView.addSubview(welcomeLabel) scrollView.addSubview(fieldLine) @@ -58,15 +58,15 @@ public class AAAuthNameViewController: AAAuthViewController { super.viewDidLoad() } - public override func viewDidLayoutSubviews() { + open override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - welcomeLabel.frame = CGRectMake(15, 90 - 66, view.width - 30, 28) - fieldLine.frame = CGRectMake(10, 200 - 66, view.width - 20, 0.5) - field.frame = CGRectMake(20, 156 - 66, view.width - 40, 44) + welcomeLabel.frame = CGRect(x: 15, y: 90 - 66, width: view.width - 30, height: 28) + fieldLine.frame = CGRect(x: 10, y: 200 - 66, width: view.width - 20, height: 0.5) + field.frame = CGRect(x: 20, y: 156 - 66, width: view.width - 40, height: 44) scrollView.frame = view.bounds - scrollView.contentSize = CGSizeMake(view.width, 240 - 66) + scrollView.contentSize = CGSize(width: view.width, height: 240 - 66) } func fieldDidChanged() { @@ -77,11 +77,11 @@ public class AAAuthNameViewController: AAAuthViewController { // } } - public override func nextDidTap() { + open override func nextDidTap() { let name = field.text!.trim() if name.length > 0 { if transactionHash != nil { - let promise = Actor.doSignupWithName(name, withSex: ACSex.UNKNOWN(), withTransaction: transactionHash!) + let promise = Actor.doSignup(withName: name, with: ACSex.unknown(), withTransaction: transactionHash!) promise.then { (r: ACAuthRes!) -> () in let promise = Actor.doCompleteAuth(r).startUserAction() promise.then { (r: JavaLangBoolean!) -> () in @@ -90,7 +90,7 @@ public class AAAuthNameViewController: AAAuthViewController { } promise.startUserAction() } else { - if ActorSDK.sharedActor().authStrategy == .PhoneOnly || ActorSDK.sharedActor().authStrategy == .PhoneEmail { + if ActorSDK.sharedActor().authStrategy == .phoneOnly || ActorSDK.sharedActor().authStrategy == .phoneEmail { navigateNext(AAAuthPhoneViewController(name: name)) } else { navigateNext(AAAuthEmailViewController(name: name)) @@ -102,7 +102,7 @@ public class AAAuthNameViewController: AAAuthViewController { } } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if isFirstAppear { @@ -111,9 +111,9 @@ public class AAAuthNameViewController: AAAuthViewController { } } - public override func viewWillDisappear(animated: Bool) { + open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) field.resignFirstResponder() } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthNavigationController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthNavigationController.swift index 560d833e0b..4952944d74 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthNavigationController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthNavigationController.swift @@ -4,31 +4,31 @@ import Foundation -public class AAAuthNavigationController: UINavigationController { +open class AAAuthNavigationController: UINavigationController { - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() navigationBar.setTransparentBackground() navigationBar.tintColor = ActorSDK.sharedActor().style.authTintColor navigationBar.hairlineHidden = true - view.backgroundColor = UIColor.whiteColor() + view.backgroundColor = UIColor.white } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - UIApplication.sharedApplication().setStatusBarStyle(.Default, animated: true) + UIApplication.shared.setStatusBarStyle(.default, animated: true) } - public override func viewWillDisappear(animated: Bool) { + open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - UIApplication.sharedApplication().setStatusBarStyle(.LightContent, animated: true) + UIApplication.shared.setStatusBarStyle(.lightContent, animated: true) } - public override func preferredStatusBarStyle() -> UIStatusBarStyle { - return .Default + open override var preferredStatusBarStyle : UIStatusBarStyle { + return .default } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthOTPViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthOTPViewController.swift index 678eb40d5f..ae5022425b 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthOTPViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthOTPViewController.swift @@ -5,9 +5,9 @@ import Foundation import MessageUI -public class AAAuthOTPViewController: AAAuthViewController, MFMailComposeViewControllerDelegate { +open class AAAuthOTPViewController: AAAuthViewController, MFMailComposeViewControllerDelegate { - private static let DIAL_SECONDS: Int = 60 + fileprivate static let DIAL_SECONDS: Int = 60 let scrollView = UIScrollView() let welcomeLabel = UILabel() @@ -24,9 +24,9 @@ public class AAAuthOTPViewController: AAAuthViewController, MFMailComposeViewCon let email: String! let phone: String! - private var counterTimer: NSTimer! - private var dialed: Bool = false - private var counter = AAAuthOTPViewController.DIAL_SECONDS + fileprivate var counterTimer: Timer! + fileprivate var dialed: Bool = false + fileprivate var counter = AAAuthOTPViewController.DIAL_SECONDS public init(email: String, transactionHash: String) { self.transactionHash = transactionHash @@ -64,12 +64,12 @@ public class AAAuthOTPViewController: AAAuthViewController, MFMailComposeViewCon fatalError("init(coder:) has not been implemented") } - public override func viewDidLoad() { + open override func viewDidLoad() { - view.backgroundColor = UIColor.whiteColor() + view.backgroundColor = UIColor.white - scrollView.keyboardDismissMode = .OnDrag - scrollView.scrollEnabled = true + scrollView.keyboardDismissMode = .onDrag + scrollView.isScrollEnabled = true scrollView.alwaysBounceVertical = true welcomeLabel.font = UIFont.lightSystemFontOfSize(23) @@ -79,47 +79,47 @@ public class AAAuthOTPViewController: AAAuthViewController, MFMailComposeViewCon welcomeLabel.text = AALocalized("AuthOTPPhoneTitle") } welcomeLabel.textColor = ActorSDK.sharedActor().style.authTitleColor - welcomeLabel.textAlignment = .Center + welcomeLabel.textAlignment = .center - validateLabel.font = UIFont.systemFontOfSize(14) + validateLabel.font = UIFont.systemFont(ofSize: 14) if email != nil { validateLabel.text = email } else { validateLabel.text = phone } validateLabel.textColor = ActorSDK.sharedActor().style.authTintColor - validateLabel.textAlignment = .Center + validateLabel.textAlignment = .center - hintLabel.font = UIFont.systemFontOfSize(14) + hintLabel.font = UIFont.systemFont(ofSize: 14) if email != nil { hintLabel.text = AALocalized("AuthOTPEmailHint") } else { hintLabel.text = AALocalized("AuthOTPPhoneHint") } hintLabel.textColor = ActorSDK.sharedActor().style.authHintColor - hintLabel.textAlignment = .Center + hintLabel.textAlignment = .center hintLabel.numberOfLines = 2 - hintLabel.lineBreakMode = .ByWordWrapping + hintLabel.lineBreakMode = .byWordWrapping - codeField.font = UIFont.systemFontOfSize(17) + codeField.font = UIFont.systemFont(ofSize: 17) codeField.textColor = ActorSDK.sharedActor().style.authTextColor codeField.placeholder = AALocalized("AuthOTPPlaceholder") - codeField.keyboardType = .NumberPad - codeField.autocapitalizationType = .None - codeField.autocorrectionType = .No + codeField.keyboardType = .numberPad + codeField.autocapitalizationType = .none + codeField.autocorrectionType = .no codeFieldLine.backgroundColor = ActorSDK.sharedActor().style.authSeparatorColor if ActorSDK.sharedActor().supportEmail != nil { - haventReceivedCode.setTitle(AALocalized("AuthOTPNoCode"), forState: .Normal) + haventReceivedCode.setTitle(AALocalized("AuthOTPNoCode"), for: UIControlState()) } else { - haventReceivedCode.hidden = true + haventReceivedCode.isHidden = true } - haventReceivedCode.titleLabel?.font = UIFont.systemFontOfSize(14) - haventReceivedCode.setTitleColor(ActorSDK.sharedActor().style.authTintColor, forState: .Normal) - haventReceivedCode.setTitleColor(ActorSDK.sharedActor().style.authTintColor.alpha(0.64), forState: .Highlighted) - haventReceivedCode.setTitleColor(ActorSDK.sharedActor().style.authHintColor, forState: .Disabled) - haventReceivedCode.addTarget(self, action: #selector(AAAuthOTPViewController.haventReceivedCodeDidPressed), forControlEvents: .TouchUpInside) + haventReceivedCode.titleLabel?.font = UIFont.systemFont(ofSize: 14) + haventReceivedCode.setTitleColor(ActorSDK.sharedActor().style.authTintColor, for: UIControlState()) + haventReceivedCode.setTitleColor(ActorSDK.sharedActor().style.authTintColor.alpha(0.64), for: .highlighted) + haventReceivedCode.setTitleColor(ActorSDK.sharedActor().style.authHintColor, for: .disabled) + haventReceivedCode.addTarget(self, action: #selector(AAAuthOTPViewController.haventReceivedCodeDidPressed), for: .touchUpInside) scrollView.addSubview(welcomeLabel) scrollView.addSubview(validateLabel) @@ -132,20 +132,20 @@ public class AAAuthOTPViewController: AAAuthViewController, MFMailComposeViewCon super.viewDidLoad() } - public override func viewDidLayoutSubviews() { + open override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - welcomeLabel.frame = CGRectMake(15, 90 - 66, view.width - 30, 28) - validateLabel.frame = CGRectMake(10, 127 - 66, view.width - 20, 17) - hintLabel.frame = CGRectMake(10, 154 - 66, view.width - 20, 56) + welcomeLabel.frame = CGRect(x: 15, y: 90 - 66, width: view.width - 30, height: 28) + validateLabel.frame = CGRect(x: 10, y: 127 - 66, width: view.width - 20, height: 17) + hintLabel.frame = CGRect(x: 10, y: 154 - 66, width: view.width - 20, height: 56) - codeField.frame = CGRectMake(20, 228 - 66, view.width - 40, 44) - codeFieldLine.frame = CGRectMake(10, 228 + 44 - 66, view.width - 20, 0.5) + codeField.frame = CGRect(x: 20, y: 228 - 66, width: view.width - 40, height: 44) + codeFieldLine.frame = CGRect(x: 10, y: 228 + 44 - 66, width: view.width - 20, height: 0.5) - haventReceivedCode.frame = CGRectMake(20, 297 - 66, view.width - 40, 56) + haventReceivedCode.frame = CGRect(x: 20, y: 297 - 66, width: view.width - 40, height: 56) scrollView.frame = view.bounds - scrollView.contentSize = CGSizeMake(view.width, 240 - 66) + scrollView.contentSize = CGSize(width: view.width, height: 240 - 66) } func haventReceivedCodeDidPressed() { @@ -168,11 +168,11 @@ public class AAAuthOTPViewController: AAAuthViewController, MFMailComposeViewCon } } - public func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?) { - controller.dismissViewControllerAnimated(true, completion: nil) + open func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + controller.dismiss(animated: true, completion: nil) } - public override func nextDidTap() { + open override func nextDidTap() { let code = codeField.text!.trim() if code.length == 0 { @@ -188,7 +188,7 @@ public class AAAuthOTPViewController: AAAuthViewController, MFMailComposeViewCon if self.name == nil { self.navigateNext(AAAuthNameViewController(transactionHash: r.transactionHash)) } else { - let promise = Actor.doSignupWithName(self.name, withSex: ACSex.UNKNOWN(), withTransaction: r.transactionHash) + let promise = Actor.doSignup(withName: self.name, with: ACSex.unknown(), withTransaction: r.transactionHash) promise.then { (r: ACAuthRes!) -> () in Actor.doCompleteAuth(r).startUserAction().then { (r: JavaLangBoolean!) -> () in self.codeField.resignFirstResponder() @@ -218,12 +218,12 @@ public class AAAuthOTPViewController: AAAuthViewController, MFMailComposeViewCon } } - private func shakeField() { + fileprivate func shakeField() { shakeView(codeField, originalX: 20) shakeView(codeFieldLine, originalX: 10) } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if self.phone != nil { @@ -231,7 +231,7 @@ public class AAAuthOTPViewController: AAAuthViewController, MFMailComposeViewCon updateTimerText() if !dialed { - counterTimer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: #selector(AAAuthOTPViewController.updateTimer), userInfo: nil, repeats: true) + counterTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(AAAuthOTPViewController.updateTimer), userInfo: nil, repeats: true) } } } @@ -247,7 +247,7 @@ public class AAAuthOTPViewController: AAAuthViewController, MFMailComposeViewCon counterTimer = nil } - Actor.doSendCodeViaCall(self.transactionHash) + Actor.doSendCode(viaCall: self.transactionHash) } updateTimerText() @@ -257,11 +257,11 @@ public class AAAuthOTPViewController: AAAuthViewController, MFMailComposeViewCon func updateTimerText() { if dialed { if ActorSDK.sharedActor().supportEmail != nil { - haventReceivedCode.setTitle(AALocalized("AuthOTPNoCode"), forState: .Normal) - haventReceivedCode.hidden = false - haventReceivedCode.enabled = true + haventReceivedCode.setTitle(AALocalized("AuthOTPNoCode"), for: UIControlState()) + haventReceivedCode.isHidden = false + haventReceivedCode.isEnabled = true } else { - haventReceivedCode.hidden = true + haventReceivedCode.isHidden = true } } else { let min = counter / 60 @@ -272,13 +272,13 @@ public class AAAuthOTPViewController: AAAuthViewController, MFMailComposeViewCon let text = AALocalized("AuthOTPCallHint") .replace("{app_name}", dest: ActorSDK.sharedActor().appName) .replace("{time}", dest: time) - haventReceivedCode.setTitle(text, forState: .Normal) - haventReceivedCode.enabled = false - haventReceivedCode.hidden = false + haventReceivedCode.setTitle(text, for: UIControlState()) + haventReceivedCode.isEnabled = false + haventReceivedCode.isHidden = false } } - public override func viewWillDisappear(animated: Bool) { + open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) if counterTimer != nil { diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthPhoneViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthPhoneViewController.swift index 81b5075515..dd546ada07 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthPhoneViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthPhoneViewController.swift @@ -36,10 +36,10 @@ class AAAuthPhoneViewController: AAAuthViewController, AACountryViewControllerDe override func viewDidLoad() { - view.backgroundColor = UIColor.whiteColor() + view.backgroundColor = UIColor.white - scrollView.keyboardDismissMode = .OnDrag - scrollView.scrollEnabled = true + scrollView.keyboardDismissMode = .onDrag + scrollView.isScrollEnabled = true scrollView.alwaysBounceVertical = true welcomeLabel.font = UIFont.lightSystemFontOfSize(23) @@ -48,31 +48,31 @@ class AAAuthPhoneViewController: AAAuthViewController, AACountryViewControllerDe welcomeLabel.numberOfLines = 1 welcomeLabel.minimumScaleFactor = 0.3 welcomeLabel.adjustsFontSizeToFitWidth = true - welcomeLabel.textAlignment = .Center + welcomeLabel.textAlignment = .center - hintLabel.font = UIFont.systemFontOfSize(14) + hintLabel.font = UIFont.systemFont(ofSize: 14) hintLabel.textColor = ActorSDK.sharedActor().style.authHintColor hintLabel.text = AALocalized("AuthPhoneHint") hintLabel.numberOfLines = 2 - hintLabel.textAlignment = .Center + hintLabel.textAlignment = .center - countryButton.setTitle(currentCountry.country, forState: .Normal) - countryButton.setTitleColor(ActorSDK.sharedActor().style.authTextColor, forState: .Normal) - countryButton.titleLabel!.font = UIFont.systemFontOfSize(17) + countryButton.setTitle(currentCountry.country, for: UIControlState()) + countryButton.setTitleColor(ActorSDK.sharedActor().style.authTextColor, for: UIControlState()) + countryButton.titleLabel!.font = UIFont.systemFont(ofSize: 17) countryButton.titleEdgeInsets = UIEdgeInsetsMake(11, 10, 11, 10) - countryButton.contentHorizontalAlignment = .Left - countryButton.setBackgroundImage(Imaging.imageWithColor(UIColor.alphaBlack(0.2), size: CGSizeMake(1, 1)), forState: .Highlighted) - countryButton.addTarget(self, action: #selector(AAAuthPhoneViewController.countryDidPressed), forControlEvents: .TouchUpInside) + countryButton.contentHorizontalAlignment = .left + countryButton.setBackgroundImage(Imaging.imageWithColor(UIColor.alphaBlack(0.2), size: CGSize(width: 1, height: 1)), for: .highlighted) + countryButton.addTarget(self, action: #selector(AAAuthPhoneViewController.countryDidPressed), for: .touchUpInside) countryButtonLine.backgroundColor = ActorSDK.sharedActor().style.authSeparatorColor - phoneCodeLabel.font = UIFont.systemFontOfSize(17) + phoneCodeLabel.font = UIFont.systemFont(ofSize: 17) phoneCodeLabel.textColor = ActorSDK.sharedActor().style.authHintColor phoneCodeLabel.text = "+\(currentCountry.code)" - phoneCodeLabel.textAlignment = .Center + phoneCodeLabel.textAlignment = .center phoneNumberLabel.currentIso = currentCountry.iso - phoneNumberLabel.keyboardType = .PhonePad + phoneNumberLabel.keyboardType = .phonePad phoneNumberLabel.placeholder = AALocalized("AuthPhonePlaceholder") phoneNumberLabel.textColor = ActorSDK.sharedActor().style.authTextColor @@ -108,7 +108,7 @@ class AAAuthPhoneViewController: AAAuthViewController, AACountryViewControllerDe if let url = ActorSDK.sharedActor().termsOfServiceUrl { self.openUrl(url) } else if let text = ActorSDK.sharedActor().termsOfServiceText { - self.presentViewController(AABigAlertController(alertTitle: tosText, alertMessage: text), animated: true, completion: nil) + self.present(AABigAlertController(alertTitle: tosText, alertMessage: text), animated: true, completion: nil) } } attributedTerms.yy_setColor(ActorSDK.sharedActor().style.authTintColor, range: tosRange) @@ -127,7 +127,7 @@ class AAAuthPhoneViewController: AAAuthViewController, AACountryViewControllerDe if let url = ActorSDK.sharedActor().privacyPolicyUrl { self.openUrl(url) } else if let text = ActorSDK.sharedActor().privacyPolicyText { - self.presentViewController(AABigAlertController(alertTitle: privacyText, alertMessage: text), animated: true, completion: nil) + self.present(AABigAlertController(alertTitle: privacyText, alertMessage: text), animated: true, completion: nil) } } attributedTerms.yy_setColor(ActorSDK.sharedActor().style.authTintColor, range: privacyRange) @@ -136,22 +136,22 @@ class AAAuthPhoneViewController: AAAuthViewController, AACountryViewControllerDe termsLabel.attributedText = attributedTerms - termsLabel.font = UIFont.systemFontOfSize(14) + termsLabel.font = UIFont.systemFont(ofSize: 14) termsLabel.numberOfLines = 2 - termsLabel.textAlignment = .Center + termsLabel.textAlignment = .center } else { - termsLabel.hidden = true + termsLabel.isHidden = true } - if ActorSDK.sharedActor().authStrategy == .EmailOnly || ActorSDK.sharedActor().authStrategy == .PhoneEmail { - useEmailButton.setTitle(AALocalized("AuthPhoneUseEmail"), forState: .Normal) - useEmailButton.titleLabel?.font = UIFont.systemFontOfSize(14) - useEmailButton.setTitleColor(ActorSDK.sharedActor().style.authTintColor, forState: .Normal) - useEmailButton.setTitleColor(ActorSDK.sharedActor().style.authTintColor.alpha(0.56), forState: .Highlighted) - useEmailButton.addTarget(self, action: #selector(AAAuthPhoneViewController.useEmailDidPressed), forControlEvents: .TouchUpInside) + if ActorSDK.sharedActor().authStrategy == .emailOnly || ActorSDK.sharedActor().authStrategy == .phoneEmail { + useEmailButton.setTitle(AALocalized("AuthPhoneUseEmail"), for: UIControlState()) + useEmailButton.titleLabel?.font = UIFont.systemFont(ofSize: 14) + useEmailButton.setTitleColor(ActorSDK.sharedActor().style.authTintColor, for: UIControlState()) + useEmailButton.setTitleColor(ActorSDK.sharedActor().style.authTintColor.alpha(0.56), for: .highlighted) + useEmailButton.addTarget(self, action: #selector(AAAuthPhoneViewController.useEmailDidPressed), for: .touchUpInside) } else { - useEmailButton.hidden = true + useEmailButton.isHidden = true } @@ -174,34 +174,34 @@ class AAAuthPhoneViewController: AAAuthViewController, AACountryViewControllerDe override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - welcomeLabel.frame = CGRectMake(20, 90 - 66, view.width - 40, 28) - hintLabel.frame = CGRectMake(20, 127 - 66, view.width - 40, 34) + welcomeLabel.frame = CGRect(x: 20, y: 90 - 66, width: view.width - 40, height: 28) + hintLabel.frame = CGRect(x: 20, y: 127 - 66, width: view.width - 40, height: 34) - countryButton.frame = CGRectMake(10, 200 - 66, view.width - 20, 44) - countryButtonLine.frame = CGRectMake(10, 244 - 66, view.width - 20, 0.5) + countryButton.frame = CGRect(x: 10, y: 200 - 66, width: view.width - 20, height: 44) + countryButtonLine.frame = CGRect(x: 10, y: 244 - 66, width: view.width - 20, height: 0.5) - termsLabel.frame = CGRectMake(20, 314 - 66, view.width - 40, 55) + termsLabel.frame = CGRect(x: 20, y: 314 - 66, width: view.width - 40, height: 55) - useEmailButton.frame = CGRectMake(20, 375 - 66, view.width - 40, 38) + useEmailButton.frame = CGRect(x: 20, y: 375 - 66, width: view.width - 40, height: 38) resizePhoneLabels() scrollView.frame = view.bounds - scrollView.contentSize = CGSizeMake(view.width, 400) + scrollView.contentSize = CGSize(width: view.width, height: 400) } - private func resizePhoneLabels() { - phoneCodeLabel.frame = CGRectMake(10, 244 - 66, 80, 44) + fileprivate func resizePhoneLabels() { + phoneCodeLabel.frame = CGRect(x: 10, y: 244 - 66, width: 80, height: 44) phoneCodeLabel.sizeToFit() - phoneCodeLabel.frame = CGRectMake(10, 244 - 66, phoneCodeLabel.width + 32, 44) + phoneCodeLabel.frame = CGRect(x: 10, y: 244 - 66, width: phoneCodeLabel.width + 32, height: 44) - phoneNumberLabel.frame = CGRectMake(phoneCodeLabel.width + 10, 245 - 66, view.width - phoneCodeLabel.width, 44) - phoneCodeLabelLine.frame = CGRectMake(10, 288 - 66, view.width - 20, 0.5) + phoneNumberLabel.frame = CGRect(x: phoneCodeLabel.width + 10, y: 245 - 66, width: view.width - phoneCodeLabel.width, height: 44) + phoneCodeLabelLine.frame = CGRect(x: 10, y: 288 - 66, width: view.width - 20, height: 0.5) } - func countriesController(countriesController: AACountryViewController, didChangeCurrentIso currentIso: String) { + func countriesController(_ countriesController: AACountryViewController, didChangeCurrentIso currentIso: String) { currentCountry = AATelephony.getCountry(currentIso) - countryButton.setTitle(currentCountry.country, forState: .Normal) + countryButton.setTitle(currentCountry.country, for: UIControlState()) phoneCodeLabel.text = "+\(currentCountry.code)" phoneNumberLabel.currentIso = currentIso resizePhoneLabels() @@ -223,17 +223,17 @@ class AAAuthPhoneViewController: AAAuthViewController, AACountryViewControllerDe let numberStr = phoneNumberLabel.phoneNumber let number = phoneNumberLabel.phoneNumber.toJLong() - Actor.doStartAuthWithPhone(number).startUserAction().then { (res: ACAuthStartRes!) -> () in + Actor.doStartAuth(withPhone: number).startUserAction().then { (res: ACAuthStartRes!) -> () in if res.authMode.toNSEnum() == .OTP { - self.navigateNext(AAAuthOTPViewController(phone: numberStr, name: self.name, transactionHash: res.transactionHash)) + self.navigateNext(AAAuthOTPViewController(phone: numberStr!, name: self.name, transactionHash: res.transactionHash)) } else { self.alertUser(AALocalized("AuthUnsupported").replace("{app_name}", dest: ActorSDK.sharedActor().appName)) } } } - override func keyboardWillAppear(height: CGFloat) { - scrollView.frame = CGRectMake(0, 0, view.width, view.height - height) + override func keyboardWillAppear(_ height: CGFloat) { + scrollView.frame = CGRect(x: 0, y: 0, width: view.width, height: view.height - height) if AADevice.isiPhone4 || AADevice.isiPhone5 { @@ -246,13 +246,13 @@ class AAAuthPhoneViewController: AAAuthViewController, AACountryViewControllerDe } override func keyboardWillDisappear() { - scrollView.frame = CGRectMake(0, 0, view.width, view.height) + scrollView.frame = CGRect(x: 0, y: 0, width: view.width, height: view.height) } - override func viewWillDisappear(animated: Bool) { + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) phoneNumberLabel.resignFirstResponder() } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthViewController.swift index c19d222650..071cbba13d 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthViewController.swift @@ -4,62 +4,62 @@ import Foundation -public class AAAuthViewController: AAViewController { +open class AAAuthViewController: AAViewController { - public let nextBarButton = UIButton() - private var keyboardHeight: CGFloat = 0 + open let nextBarButton = UIButton() + fileprivate var keyboardHeight: CGFloat = 0 - override public func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() - nextBarButton.setTitle(AALocalized("NavigationNext"), forState: .Normal) - nextBarButton.setTitleColor(UIColor.whiteColor(), forState: .Normal) - nextBarButton.setBackgroundImage(Imaging.roundedImage(UIColor(red: 94, green: 142, blue: 192), radius: 4), forState: .Normal) - nextBarButton.setBackgroundImage(Imaging.roundedImage(UIColor(red: 94, green: 142, blue: 192).alpha(0.7), radius: 4), forState: .Highlighted) - nextBarButton.addTarget(self, action: #selector(AAAuthViewController.nextDidTap), forControlEvents: .TouchUpInside) + nextBarButton.setTitle(AALocalized("NavigationNext"), for: UIControlState()) + nextBarButton.setTitleColor(UIColor.white, for: UIControlState()) + nextBarButton.setBackgroundImage(Imaging.roundedImage(UIColor(red: 94, green: 142, blue: 192), radius: 4), for: UIControlState()) + nextBarButton.setBackgroundImage(Imaging.roundedImage(UIColor(red: 94, green: 142, blue: 192).alpha(0.7), radius: 4), for: .highlighted) + nextBarButton.addTarget(self, action: #selector(AAAuthViewController.nextDidTap), for: .touchUpInside) view.addSubview(nextBarButton) } - override public func viewDidLayoutSubviews() { + override open func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() layoutNextBar() } - override public func viewWillAppear(animated: Bool) { + override open func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // Forcing initial layout before keyboard show to avoid weird animations layoutNextBar() - NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(AAAuthViewController.keyboardWillAppearInt(_:)), name: UIKeyboardWillShowNotification, object: nil) - NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(AAAuthViewController.keyboardWillDisappearInt(_:)), name: UIKeyboardWillHideNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(AAAuthViewController.keyboardWillAppearInt(_:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(AAAuthViewController.keyboardWillDisappearInt(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil) } - private func layoutNextBar() { - nextBarButton.frame = CGRectMake(view.width - 95, view.height - 44 - keyboardHeight + 6, 85, 32) + fileprivate func layoutNextBar() { + nextBarButton.frame = CGRect(x: view.width - 95, y: view.height - 44 - keyboardHeight + 6, width: 85, height: 32) } - func keyboardWillAppearInt(notification: NSNotification) { - let dict = notification.userInfo! - let rect = dict[UIKeyboardFrameBeginUserInfoKey]!.CGRectValue + func keyboardWillAppearInt(_ notification: Notification) { + let dict = (notification as NSNotification).userInfo! + let rect = (dict[UIKeyboardFrameBeginUserInfoKey]! as AnyObject).cgRectValue - let orientation = UIApplication.sharedApplication().statusBarOrientation - let frameInWindow = self.view.superview!.convertRect(view.bounds, toView: nil) + let orientation = UIApplication.shared.statusBarOrientation + let frameInWindow = self.view.superview!.convert(view.bounds, to: nil) let windowBounds = view.window!.bounds - let keyboardTop: CGFloat = windowBounds.size.height - rect.height + let keyboardTop: CGFloat = windowBounds.size.height - rect!.height let heightCoveredByKeyboard: CGFloat if AADevice.isiPad { - if orientation == .LandscapeLeft || orientation == .LandscapeRight { + if orientation == .landscapeLeft || orientation == .landscapeRight { heightCoveredByKeyboard = frameInWindow.maxY - keyboardTop - 52 /*???*/ - } else if orientation == .Portrait || orientation == .PortraitUpsideDown { - heightCoveredByKeyboard = CGRectGetMaxY(frameInWindow) - keyboardTop + } else if orientation == .portrait || orientation == .portraitUpsideDown { + heightCoveredByKeyboard = frameInWindow.maxY - keyboardTop } else { heightCoveredByKeyboard = 0 } } else { - heightCoveredByKeyboard = rect.height + heightCoveredByKeyboard = (rect?.height)! } keyboardHeight = max(0, heightCoveredByKeyboard) @@ -68,34 +68,34 @@ public class AAAuthViewController: AAViewController { } - func keyboardWillDisappearInt(notification: NSNotification) { + func keyboardWillDisappearInt(_ notification: Notification) { keyboardHeight = 0 layoutNextBar() keyboardWillDisappear() } - public func keyboardWillAppear(height: CGFloat) { + open func keyboardWillAppear(_ height: CGFloat) { } - public func keyboardWillDisappear() { + open func keyboardWillDisappear() { } - override public func viewWillDisappear(animated: Bool) { + override open func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - NSNotificationCenter.defaultCenter().removeObserver(self) + NotificationCenter.default.removeObserver(self) keyboardHeight = 0 layoutNextBar() } /// Call this method when authentication successful - public func onAuthenticated() { + open func onAuthenticated() { ActorSDK.sharedActor().didLoggedIn() } - public func nextDidTap() { + open func nextDidTap() { } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthWelcomeController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthWelcomeController.swift index 57ceefd074..2ff4c26b62 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthWelcomeController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AAAuthWelcomeController.swift @@ -4,7 +4,7 @@ import UIKit -public class AAWelcomeController: AAViewController { +open class AAWelcomeController: AAViewController { let bgImage: UIImageView = UIImageView() let logoView: UIImageView = UIImageView() @@ -23,44 +23,44 @@ public class AAWelcomeController: AAViewController { fatalError("init(coder:) has not been implemented") } - public override func loadView() { + open override func loadView() { super.loadView() self.view.backgroundColor = ActorSDK.sharedActor().style.welcomeBgColor self.bgImage.image = ActorSDK.sharedActor().style.welcomeBgImage - self.bgImage.hidden = ActorSDK.sharedActor().style.welcomeBgImage == nil - self.bgImage.contentMode = .ScaleAspectFill + self.bgImage.isHidden = ActorSDK.sharedActor().style.welcomeBgImage == nil + self.bgImage.contentMode = .scaleAspectFill self.logoView.image = ActorSDK.sharedActor().style.welcomeLogo self.size = ActorSDK.sharedActor().style.welcomeLogoSize self.logoViewVerticalGap = ActorSDK.sharedActor().style.logoViewVerticalGap appNameLabel.text = AALocalized("WelcomeTitle").replace("{app_name}", dest: ActorSDK.sharedActor().appName) - appNameLabel.textAlignment = .Center - appNameLabel.backgroundColor = UIColor.clearColor() + appNameLabel.textAlignment = .center + appNameLabel.backgroundColor = UIColor.clear appNameLabel.font = UIFont.mediumSystemFontOfSize(24) appNameLabel.textColor = ActorSDK.sharedActor().style.welcomeTitleColor someInfoLabel.text = AALocalized("WelcomeTagline") - someInfoLabel.textAlignment = .Center - someInfoLabel.backgroundColor = UIColor.clearColor() - someInfoLabel.font = UIFont.systemFontOfSize(16) + someInfoLabel.textAlignment = .center + someInfoLabel.backgroundColor = UIColor.clear + someInfoLabel.font = UIFont.systemFont(ofSize: 16) someInfoLabel.numberOfLines = 2 someInfoLabel.textColor = ActorSDK.sharedActor().style.welcomeTaglineColor - signupButton.setTitle(AALocalized("WelcomeSignUp"), forState: .Normal) + signupButton.setTitle(AALocalized("WelcomeSignUp"), for: UIControlState()) signupButton.titleLabel?.font = UIFont.mediumSystemFontOfSize(17) - signupButton.setTitleColor(ActorSDK.sharedActor().style.welcomeSignupTextColor, forState: .Normal) - signupButton.setBackgroundImage(Imaging.roundedImage(ActorSDK.sharedActor().style.welcomeSignupBgColor, radius: 22), forState: .Normal) - signupButton.setBackgroundImage(Imaging.roundedImage(ActorSDK.sharedActor().style.welcomeSignupBgColor.alpha(0.7), radius: 22), forState: .Highlighted) - signupButton.addTarget(self, action: #selector(AAWelcomeController.signupAction), forControlEvents: UIControlEvents.TouchUpInside) + signupButton.setTitleColor(ActorSDK.sharedActor().style.welcomeSignupTextColor, for: UIControlState()) + signupButton.setBackgroundImage(Imaging.roundedImage(ActorSDK.sharedActor().style.welcomeSignupBgColor, radius: 22), for: UIControlState()) + signupButton.setBackgroundImage(Imaging.roundedImage(ActorSDK.sharedActor().style.welcomeSignupBgColor.alpha(0.7), radius: 22), for: .highlighted) + signupButton.addTarget(self, action: #selector(AAWelcomeController.signupAction), for: UIControlEvents.touchUpInside) - signinButton.setTitle(AALocalized("WelcomeLogIn"), forState: .Normal) - signinButton.titleLabel?.font = UIFont.systemFontOfSize(17) - signinButton.setTitleColor(ActorSDK.sharedActor().style.welcomeLoginTextColor, forState: .Normal) - signinButton.setTitleColor(ActorSDK.sharedActor().style.welcomeLoginTextColor.alpha(0.7), forState: .Highlighted) - signinButton.addTarget(self, action: #selector(AAWelcomeController.signInAction), forControlEvents: UIControlEvents.TouchUpInside) + signinButton.setTitle(AALocalized("WelcomeLogIn"), for: UIControlState()) + signinButton.titleLabel?.font = UIFont.systemFont(ofSize: 17) + signinButton.setTitleColor(ActorSDK.sharedActor().style.welcomeLoginTextColor, for: UIControlState()) + signinButton.setTitleColor(ActorSDK.sharedActor().style.welcomeLoginTextColor.alpha(0.7), for: .highlighted) + signinButton.addTarget(self, action: #selector(AAWelcomeController.signInAction), for: UIControlEvents.touchUpInside) self.view.addSubview(self.bgImage) self.view.addSubview(self.logoView) @@ -70,45 +70,45 @@ public class AAWelcomeController: AAViewController { self.view.addSubview(self.signinButton) } - override public func viewDidLayoutSubviews() { + override open func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() if AADevice.isiPhone4 { - logoView.frame = CGRectMake((view.width - size.width) / 2, 90, size.width, size.height) - appNameLabel.frame = CGRectMake((view.width - 300) / 2, logoView.bottom + 30, 300, 29) - someInfoLabel.frame = CGRectMake((view.width - 300) / 2, appNameLabel.bottom + 8, 300, 56) + logoView.frame = CGRect(x: (view.width - size.width) / 2, y: 90, width: size.width, height: size.height) + appNameLabel.frame = CGRect(x: (view.width - 300) / 2, y: logoView.bottom + 30, width: 300, height: 29) + someInfoLabel.frame = CGRect(x: (view.width - 300) / 2, y: appNameLabel.bottom + 8, width: 300, height: 56) - signupButton.frame = CGRectMake((view.width - 136) / 2, view.height - 44 - 80, 136, 44) - signinButton.frame = CGRectMake((view.width - 136) / 2, view.height - 44 - 25, 136, 44) + signupButton.frame = CGRect(x: (view.width - 136) / 2, y: view.height - 44 - 80, width: 136, height: 44) + signinButton.frame = CGRect(x: (view.width - 136) / 2, y: view.height - 44 - 25, width: 136, height: 44) } else { - logoView.frame = CGRectMake((view.width - size.width) / 2, logoViewVerticalGap, size.width, size.height) - appNameLabel.frame = CGRectMake((view.width - 300) / 2, logoView.bottom + 35, 300, 29) - someInfoLabel.frame = CGRectMake((view.width - 300) / 2, appNameLabel.bottom + 8, 300, 56) + logoView.frame = CGRect(x: (view.width - size.width) / 2, y: logoViewVerticalGap, width: size.width, height: size.height) + appNameLabel.frame = CGRect(x: (view.width - 300) / 2, y: logoView.bottom + 35, width: 300, height: 29) + someInfoLabel.frame = CGRect(x: (view.width - 300) / 2, y: appNameLabel.bottom + 8, width: 300, height: 56) - signupButton.frame = CGRectMake((view.width - 136) / 2, view.height - 44 - 90, 136, 44) - signinButton.frame = CGRectMake((view.width - 136) / 2, view.height - 44 - 35, 136, 44) + signupButton.frame = CGRect(x: (view.width - 136) / 2, y: view.height - 44 - 90, width: 136, height: 44) + signinButton.frame = CGRect(x: (view.width - 136) / 2, y: view.height - 44 - 35, width: 136, height: 44) } self.bgImage.frame = view.bounds } - public func signupAction() { + open func signupAction() { // TODO: Remove BG after auth? - UIApplication.sharedApplication().keyWindow?.backgroundColor = ActorSDK.sharedActor().style.welcomeBgColor + UIApplication.shared.keyWindow?.backgroundColor = ActorSDK.sharedActor().style.welcomeBgColor self.presentElegantViewController(AAAuthNavigationController(rootViewController: AAAuthNameViewController())) } - public func signInAction() { + open func signInAction() { // TODO: Remove BG after auth? - UIApplication.sharedApplication().keyWindow?.backgroundColor = ActorSDK.sharedActor().style.welcomeBgColor + UIApplication.shared.keyWindow?.backgroundColor = ActorSDK.sharedActor().style.welcomeBgColor self.presentElegantViewController(AAAuthNavigationController(rootViewController: AAAuthLogInViewController())) } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // TODO: Fix after cancel? - UIApplication.sharedApplication().setStatusBarStyle(.LightContent, animated: true) + UIApplication.shared.setStatusBarStyle(.lightContent, animated: true) } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AABigAlertController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AABigAlertController.swift index 308a62d809..8d20758d68 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AABigAlertController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AABigAlertController.swift @@ -6,21 +6,21 @@ import Foundation import UIKit -public class AABigAlertController: UIViewController,UIViewControllerTransitioningDelegate { +open class AABigAlertController: UIViewController,UIViewControllerTransitioningDelegate { - private let alertTitle: String - private let alertMessage: String + fileprivate let alertTitle: String + fileprivate let alertMessage: String - private var alertView : UIView! - private var alertTitleLabel : UILabel! - private var alertTextView : UITextView! - private var buttonOk : UIButton! + fileprivate var alertView : UIView! + fileprivate var alertTitleLabel : UILabel! + fileprivate var alertTextView : UITextView! + fileprivate var buttonOk : UIButton! public init(alertTitle: String, alertMessage: String) { self.alertTitle = alertTitle self.alertMessage = alertMessage super.init(nibName: nil, bundle: nil) - self.modalPresentationStyle = .Custom + self.modalPresentationStyle = .custom self.transitioningDelegate = self } @@ -28,66 +28,66 @@ public class AABigAlertController: UIViewController,UIViewControllerTransitionin fatalError("init(coder:) has not been implemented") } - public override func loadView() { + open override func loadView() { super.loadView() self.alertView = UIView() - self.alertView.frame = CGRectMake(self.view.frame.width/2 - 120, self.view.frame.height/2 - 165, 240, 330) - self.alertView.backgroundColor = UIColor.whiteColor() + self.alertView.frame = CGRect(x: self.view.frame.width/2 - 120, y: self.view.frame.height/2 - 165, width: 240, height: 330) + self.alertView.backgroundColor = UIColor.white self.alertView.layer.cornerRadius = 10 self.alertView.layer.masksToBounds = true self.view.addSubview(self.alertView) self.alertTitleLabel = UILabel() - self.alertTitleLabel.font = UIFont.boldSystemFontOfSize(17) - self.alertTitleLabel.frame = CGRectMake(10,10,220,30) + self.alertTitleLabel.font = UIFont.boldSystemFont(ofSize: 17) + self.alertTitleLabel.frame = CGRect(x: 10,y: 10,width: 220,height: 30) self.alertTitleLabel.text = alertTitle - self.alertTitleLabel.backgroundColor = UIColor.clearColor() - self.alertTitleLabel.textAlignment = .Center + self.alertTitleLabel.backgroundColor = UIColor.clear + self.alertTitleLabel.textAlignment = .center self.alertView.addSubview(self.alertTitleLabel) self.alertTextView = UITextView() self.alertTextView.font = UIFont.lightSystemFontOfSize(13) - self.alertTextView.backgroundColor = UIColor.clearColor() - self.alertTextView.editable = false + self.alertTextView.backgroundColor = UIColor.clear + self.alertTextView.isEditable = false self.alertTextView.text = alertMessage - self.alertTextView.frame = CGRectMake(10, 45, 220, 245); - self.alertTextView.userInteractionEnabled = true + self.alertTextView.frame = CGRect(x: 10, y: 45, width: 220, height: 245); + self.alertTextView.isUserInteractionEnabled = true self.alertView.addSubview(self.alertTextView) let separatorView = UIView() - separatorView.frame = CGRectMake(0, 290, 240, 0.5) - separatorView.backgroundColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.5) + separatorView.frame = CGRect(x: 0, y: 290, width: 240, height: 0.5) + separatorView.backgroundColor = UIColor.lightGray.withAlphaComponent(0.5) self.alertView.addSubview(separatorView) - self.buttonOk = UIButton(type: UIButtonType.System) - self.buttonOk.setTitle(AALocalized("AlertOk"), forState: UIControlState.Normal) - self.buttonOk.setTitleColor(UIColor.blueColor(), forState: UIControlState.Normal) - self.buttonOk.frame = CGRectMake(0,291,240,39) - self.buttonOk.addTarget(self, action: #selector(AABigAlertController.closeController), forControlEvents: UIControlEvents.TouchUpInside) + self.buttonOk = UIButton(type: UIButtonType.system) + self.buttonOk.setTitle(AALocalized("AlertOk"), for: UIControlState()) + self.buttonOk.setTitleColor(UIColor.blue, for: UIControlState()) + self.buttonOk.frame = CGRect(x: 0,y: 291,width: 240,height: 39) + self.buttonOk.addTarget(self, action: #selector(AABigAlertController.closeController), for: UIControlEvents.touchUpInside) self.alertView.addSubview(self.buttonOk) } - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() let touch = UITapGestureRecognizer(target: self, action: #selector(AABigAlertController.closeController)) self.view.addGestureRecognizer(touch) } - public func closeController() { - self.dismissViewControllerAnimated(true, completion: nil) + open func closeController() { + self.dismiss(animated: true, completion: nil) } - public func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController, sourceViewController source: UIViewController) -> UIPresentationController? { + open func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { if presented == self { - return AACustomPresentationController(presentedViewController: presented, presentingViewController: presenting) + return AACustomPresentationController(presentedViewController: presented, presenting: presenting) } return nil } - public func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + open func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { if presented == self { return AACustomPresentationAnimationController(isPresenting: true) @@ -97,7 +97,7 @@ public class AABigAlertController: UIViewController,UIViewControllerTransitionin } } - public func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + open func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { if dismissed == self { return AACustomPresentationAnimationController(isPresenting: false) @@ -106,4 +106,4 @@ public class AABigAlertController: UIViewController,UIViewControllerTransitionin return nil } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AACountryViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AACountryViewController.swift index 5a94430c14..09cbeae540 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AACountryViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/AACountryViewController.swift @@ -5,85 +5,89 @@ import UIKit public protocol AACountryViewControllerDelegate { - func countriesController(countriesController: AACountryViewController, didChangeCurrentIso currentIso: String) + func countriesController(_ countriesController: AACountryViewController, didChangeCurrentIso currentIso: String) } -public class AACountryViewController: AATableViewController { +open class AACountryViewController: AATableViewController { - private var _countries: NSDictionary! - private var _letters: NSArray! + fileprivate var _countries: NSDictionary! + fileprivate var _letters: Array! - public var delegate: AACountryViewControllerDelegate? + open var delegate: AACountryViewControllerDelegate? public init() { - super.init(style: UITableViewStyle.Plain) + super.init(style: UITableViewStyle.plain) self.title = AALocalized("AuthCountryTitle") - let cancelButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: UIBarButtonItemStyle.Plain, target: self, action: Selector("dismiss")) - self.navigationItem.setLeftBarButtonItem(cancelButtonItem, animated: false) + let cancelButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: UIBarButtonItemStyle.plain, target: self, action: #selector(AAViewController.dismissController)) + self.navigationItem.setLeftBarButton(cancelButtonItem, animated: false) - self.content = ACAllEvents_Auth.AUTH_PICK_COUNTRY() + self.content = ACAllEvents_Auth.auth_PICK_COUNTRY() } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() tableView.rowHeight = 44.0 - tableView.sectionIndexBackgroundColor = UIColor.clearColor() + tableView.sectionIndexBackgroundColor = UIColor.clear } - private func countries() -> NSDictionary { + fileprivate func countries() -> NSDictionary { if (_countries == nil) { let countries = NSMutableDictionary() - for (_, iso) in ABPhoneField.sortedIsoCodes().enumerate() { + for (_, iso) in ABPhoneField.sortedIsoCodes().enumerated() { let countryName = ABPhoneField.countryNameByCountryCode()[iso as! String] as! String let phoneCode = ABPhoneField.callingCodeByCountryCode()[iso as! String] as! String // if (self.searchBar.text.length == 0 || [countryName rangeOfString:self.searchBar.text options:NSCaseInsensitiveSearch].location != NSNotFound) - let countryLetter = countryName.substringToIndex(countryName.startIndex.advancedBy(1)) + let countryLetter = countryName.substring(to: countryName.characters.index(countryName.startIndex, offsetBy: 1)) if (countries[countryLetter] == nil) { countries[countryLetter] = NSMutableArray() } - countries[countryLetter]!.addObject([countryName, iso, phoneCode]) + (countries[countryLetter]! as AnyObject).add([countryName, iso, phoneCode]) } _countries = countries; } return _countries; } - private func letters() -> NSArray { + fileprivate func letters() -> Array { if (_letters == nil) { - _letters = (countries().allKeys as NSArray).sortedArrayUsingSelector(#selector(YYTextPosition.compare(_:))) + _letters = (countries().allKeys as! [String]).sorted(by: { (a: String, b: String) -> Bool in + return a < b + }) } - return _letters; + return _letters } - public func sectionIndexTitlesForTableView(tableView: UITableView) -> [AnyObject]! { + open func sectionIndexTitlesForTableView(_ tableView: UITableView) -> [AnyObject]! { return letters() as [AnyObject] } - public func tableView(tableView: UITableView, sectionForSectionIndexTitle title: String, atIndex index: Int) -> Int { + open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, atIndex index: Int) -> Int { return index } - public override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + open override func numberOfSections(in tableView: UITableView) -> Int { return letters().count; } - public override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return (countries()[letters()[section] as! String] as! NSArray).count + open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let letter = letters()[section] + let cs = countries().object(forKey: letter) as! NSArray + return cs.count } - public override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: AAAuthCountryCell = tableView.dequeueCell(indexPath) - let letter = letters()[indexPath.section] as! String - let countryData = (countries().objectForKey(letter) as! NSArray)[indexPath.row] as! [String] + let letter = letters()[(indexPath as NSIndexPath).section] + let countryData = (countries().object(forKey: letter) as! NSArray)[(indexPath as NSIndexPath).row] as! [String] cell.setTitle(countryData[0]) cell.setCode("+\(countryData[2])") @@ -92,28 +96,28 @@ public class AACountryViewController: AATableViewController { return cell } - public func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { - tableView.deselectRowAtIndexPath(indexPath, animated: true) + open func tableView(_ tableView: UITableView, didSelectRowAtIndexPath indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) - let letter = letters()[indexPath.section] as! String - let countryData = (countries().objectForKey(letter) as! NSArray)[indexPath.row] as! [String] + let letter = letters()[(indexPath as NSIndexPath).section] + let countryData = (countries().object(forKey: letter) as! NSArray)[(indexPath as NSIndexPath).row] as! [String] delegate?.countriesController(self, didChangeCurrentIso: countryData[1]) - dismiss() + dismissController() } - public func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return 25.0 } - public func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - return letters()[section] as? String + open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return letters()[section] } - public override func viewWillDisappear(animated: Bool) { + open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - UIApplication.sharedApplication().setStatusBarStyle(.Default, animated: true) + UIApplication.shared.setStatusBarStyle(.default, animated: true) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/Cells/AAAuthCountryCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/Cells/AAAuthCountryCell.swift index 21cd7091a9..0e5da9bffb 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/Cells/AAAuthCountryCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Auth/Cells/AAAuthCountryCell.swift @@ -4,18 +4,18 @@ import UIKit -public class AAAuthCountryCell: UITableViewCell { +open class AAAuthCountryCell: UITableViewCell { // MARK: - // MARK: Private vars - private var titleLabel: UILabel! - private var codeLabel: UILabel! + fileprivate var titleLabel: UILabel! + fileprivate var codeLabel: UILabel! // MARK: - // MARK: Public vars - public var searchMode: Bool! + open var searchMode: Bool! // MARK: - // MARK: Constructors @@ -24,19 +24,19 @@ public class AAAuthCountryCell: UITableViewCell { super.init(style: style, reuseIdentifier: reuseIdentifier) titleLabel = UILabel() - titleLabel.autoresizingMask = UIViewAutoresizing.FlexibleWidth - titleLabel.font = UIFont.systemFontOfSize(17.0) - titleLabel.textColor = UIColor.blackColor() - titleLabel.backgroundColor = UIColor.whiteColor() + titleLabel.autoresizingMask = UIViewAutoresizing.flexibleWidth + titleLabel.font = UIFont.systemFont(ofSize: 17.0) + titleLabel.textColor = UIColor.black + titleLabel.backgroundColor = UIColor.white contentView.addSubview(titleLabel) codeLabel = UILabel() - codeLabel.font = UIFont.boldSystemFontOfSize(17) - codeLabel.backgroundColor = UIColor.whiteColor() - codeLabel.textColor = UIColor.blackColor() - codeLabel.autoresizingMask = UIViewAutoresizing.FlexibleLeftMargin - codeLabel.contentMode = UIViewContentMode.Right - codeLabel.textAlignment = NSTextAlignment.Right + codeLabel.font = UIFont.boldSystemFont(ofSize: 17) + codeLabel.backgroundColor = UIColor.white + codeLabel.textColor = UIColor.black + codeLabel.autoresizingMask = UIViewAutoresizing.flexibleLeftMargin + codeLabel.contentMode = UIViewContentMode.right + codeLabel.textAlignment = NSTextAlignment.right contentView.addSubview(codeLabel) } @@ -47,15 +47,15 @@ public class AAAuthCountryCell: UITableViewCell { // MARK: - // MARK: Setters - public func setTitle(title: String) { + open func setTitle(_ title: String) { titleLabel.text = title } - public func setCode(code: String) { + open func setCode(_ code: String) { codeLabel.text = code } - public func setSearchMode(searchMode: Bool) { + open func setSearchMode(_ searchMode: Bool) { self.searchMode = searchMode let codeLabelWidth: CGFloat = 50.0 diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Calls/AACallViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Calls/AACallViewController.swift index faa8cc7ea6..444ed1e7a3 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Calls/AACallViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Calls/AACallViewController.swift @@ -5,14 +5,14 @@ import Foundation import AVFoundation -public class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { +open class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { - public let binder = AABinder() - public let callId: jlong - public let call: ACCallVM - public let senderAvatar: AAAvatarView = AAAvatarView() - public let peerTitle = UILabel() - public let callState = UILabel() + open let binder = AABinder() + open let callId: jlong + open let call: ACCallVM + open let senderAvatar: AAAvatarView = AAAvatarView() + open let peerTitle = UILabel() + open let callState = UILabel() var remoteView = RTCEAGLVideoView() var remoteVideoSize: CGSize! @@ -22,16 +22,16 @@ public class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { var localVideoTrack: RTCVideoTrack! var remoteVideoTrack: RTCVideoTrack! - public let answerCallButton = UIButton() - public let answerCallButtonText = UILabel() - public let declineCallButton = UIButton() - public let declineCallButtonText = UILabel() + open let answerCallButton = UIButton() + open let answerCallButtonText = UILabel() + open let declineCallButton = UIButton() + open let declineCallButtonText = UILabel() - public let muteButton = AACircleButton(size: 72) - public let videoButton = AACircleButton(size: 72) + open let muteButton = AACircleButton(size: 72) + // public let videoButton = AACircleButton(size: 72) var isScheduledDispose = false - var timer: NSTimer? + var timer: Timer? public init(callId: jlong) { self.callId = callId @@ -44,7 +44,7 @@ public class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { fatalError("init(coder:) has not been implemented") } - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = UIColor(rgb: 0x2a4463) @@ -53,28 +53,28 @@ public class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { // Buttons // - answerCallButton.setImage(UIImage.bundled("ic_call_answer_44")!.tintImage(UIColor.whiteColor()), forState: .Normal) - answerCallButton.setBackgroundImage(Imaging.roundedImage(UIColor(red: 61/255.0, green: 217/255.0, blue: 90/255.0, alpha: 1.0), size: CGSizeMake(74, 74), radius: 37), forState: .Normal) + answerCallButton.setImage(UIImage.bundled("ic_call_answer_44")!.tintImage(UIColor.white), for: UIControlState()) + answerCallButton.setBackgroundImage(Imaging.roundedImage(UIColor(red: 61/255.0, green: 217/255.0, blue: 90/255.0, alpha: 1.0), size: CGSize(width: 74, height: 74), radius: 37), for: UIControlState()) answerCallButton.viewDidTap = { - Actor.answerCallWithCallId(self.callId) + Actor.answerCall(withCallId: self.callId) } answerCallButtonText.font = UIFont.thinSystemFontOfSize(16) - answerCallButtonText.textColor = UIColor.whiteColor() - answerCallButtonText.textAlignment = NSTextAlignment.Center + answerCallButtonText.textColor = UIColor.white + answerCallButtonText.textAlignment = NSTextAlignment.center answerCallButtonText.text = AALocalized("CallsAnswer") - answerCallButtonText.bounds = CGRectMake(0, 0, 72, 44) + answerCallButtonText.bounds = CGRect(x: 0, y: 0, width: 72, height: 44) answerCallButtonText.numberOfLines = 2 - declineCallButton.setImage(UIImage.bundled("ic_call_end_44")!.tintImage(UIColor.whiteColor()), forState: .Normal) - declineCallButton.setBackgroundImage(Imaging.roundedImage(UIColor(red: 217/255.0, green: 80/255.0, blue:61/255.0, alpha: 1.0), size: CGSizeMake(74, 74), radius: 37), forState: .Normal) + declineCallButton.setImage(UIImage.bundled("ic_call_end_44")!.tintImage(UIColor.white), for: UIControlState()) + declineCallButton.setBackgroundImage(Imaging.roundedImage(UIColor(red: 217/255.0, green: 80/255.0, blue:61/255.0, alpha: 1.0), size: CGSize(width: 74, height: 74), radius: 37), for: UIControlState()) declineCallButton.viewDidTap = { - Actor.endCallWithCallId(self.callId) + Actor.endCall(withCallId: self.callId) } declineCallButtonText.font = UIFont.thinSystemFontOfSize(16) - declineCallButtonText.textColor = UIColor.whiteColor() - declineCallButtonText.textAlignment = NSTextAlignment.Center + declineCallButtonText.textColor = UIColor.white + declineCallButtonText.textAlignment = NSTextAlignment.center declineCallButtonText.text = AALocalized("CallsDecline") - declineCallButtonText.bounds = CGRectMake(0, 0, 72, 44) + declineCallButtonText.bounds = CGRect(x: 0, y: 0, width: 72, height: 44) declineCallButtonText.numberOfLines = 2 @@ -86,49 +86,49 @@ public class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { muteButton.title = AALocalized("CallsMute") muteButton.alpha = 0 muteButton.button.viewDidTap = { - Actor.toggleCallMuteWithCallId(self.callId) + Actor.toggleCallMute(withCallId: self.callId) } - videoButton.image = UIImage.bundled("ic_video_44") - videoButton.title = AALocalized("CallsVideo") - videoButton.alpha = 0 + // videoButton.image = UIImage.bundled("ic_video_44") + // videoButton.title = AALocalized("CallsVideo") + // videoButton.alpha = 0 - videoButton.button.viewDidTap = { - Actor.toggleVideoEnabledWithCallId(self.callId) - } + // videoButton.button.viewDidTap = { + // Actor.toggleVideoEnabledWithCallId(self.callId) + // } // // Video ViewPorts // - localView.backgroundColor = UIColor.whiteColor() + localView.backgroundColor = UIColor.white localView.alpha = 0 localView.layer.cornerRadius = 15 localView.layer.borderWidth = 1 - localView.layer.borderColor = UIColor.grayColor().CGColor + localView.layer.borderColor = UIColor.gray.cgColor localView.layer.shadowRadius = 1 localView.clipsToBounds = true - localView.contentMode = .ScaleAspectFit + localView.contentMode = .scaleAspectFit remoteView.alpha = 0 - remoteView.backgroundColor = UIColor.blackColor() + remoteView.backgroundColor = UIColor.black remoteView.delegate = self - remoteView.contentMode = .ScaleAspectFit + remoteView.contentMode = .scaleAspectFit // // Peer Info // - peerTitle.textColor = UIColor.whiteColor().alpha(0.87) - peerTitle.textAlignment = NSTextAlignment.Center + peerTitle.textColor = UIColor.white.alpha(0.87) + peerTitle.textAlignment = NSTextAlignment.center peerTitle.font = UIFont.thinSystemFontOfSize(42) peerTitle.minimumScaleFactor = 0.3 - callState.textColor = UIColor.whiteColor() - callState.textAlignment = NSTextAlignment.Center - callState.font = UIFont.systemFontOfSize(19) + callState.textColor = UIColor.white + callState.textAlignment = NSTextAlignment.center + callState.font = UIFont.systemFont(ofSize: 19) self.view.addSubview(senderAvatar) @@ -140,22 +140,22 @@ public class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { self.view.addSubview(declineCallButton) self.view.addSubview(declineCallButtonText) self.view.addSubview(muteButton) - self.view.addSubview(videoButton) + // self.view.addSubview(videoButton) self.view.addSubview(localView) } - public override func viewWillLayoutSubviews() { + open override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() - senderAvatar.frame = CGRectMake((self.view.width - 104) / 2, 60, 108, 108) - peerTitle.frame = CGRectMake(22, senderAvatar.bottom + 22, view.width - 44, 42) - callState.frame = CGRectMake(0, peerTitle.bottom + 8, view.width, 22) + senderAvatar.frame = CGRect(x: (self.view.width - 104) / 2, y: 60, width: 108, height: 108) + peerTitle.frame = CGRect(x: 22, y: senderAvatar.bottom + 22, width: view.width - 44, height: 42) + callState.frame = CGRect(x: 0, y: peerTitle.bottom + 8, width: view.width, height: 22) layoutButtons() layoutVideo() } - public func videoView(videoView: RTCEAGLVideoView!, didChangeVideoSize size: CGSize) { + open func videoView(_ videoView: RTCEAGLVideoView!, didChangeVideoSize size: CGSize) { if videoView == remoteView { self.remoteVideoSize = size } else if videoView == localView { @@ -165,46 +165,52 @@ public class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { layoutVideo() } - private func layoutButtons() { - - muteButton.frame = CGRectMake((self.view.width / 3 - 84) / 2, self.view.height - 72 - 49, 84, 72 + 5 + 44) - videoButton.frame = CGRectMake(2 * self.view.width / 3 + (self.view.width / 3 - 84) / 2, self.view.height - 72 - 49, 84, 72 + 5 + 44) - - if !declineCallButton.hidden || !answerCallButton.hidden { - if !declineCallButton.hidden && !answerCallButton.hidden { - declineCallButton.frame = CGRectMake(25, self.view.height - 72 - 49, 72, 72) + fileprivate func layoutButtons() { + + muteButton.frame = CGRect(x: (self.view.width / 3 - 84) / 2, y: self.view.height - 72 - 49, width: 84, height: 72 + 5 + 44) +// videoButton.frame = CGRectMake(2 * self.view.width / 3 + (self.view.width / 3 - 84) / 2, self.view.height - 72 - 49, 84, 72 + 5 + 44) +// if call.isVideoPreferred.boolValue { +// videoButton.hidden = true +// } else { +// videoButton.hidden = false +// } + + if !declineCallButton.isHidden || !answerCallButton.isHidden { + if !declineCallButton.isHidden && !answerCallButton.isHidden { + declineCallButton.frame = CGRect(x: 25, y: self.view.height - 72 - 49, width: 72, height: 72) declineCallButtonText.under(declineCallButton.frame, offset: 5) - answerCallButton.frame = CGRectMake(self.view.width - 72 - 25, self.view.height - 72 - 49, 72, 72) + answerCallButton.frame = CGRect(x: self.view.width - 72 - 25, y: self.view.height - 72 - 49, width: 72, height: 72) answerCallButtonText.under(answerCallButton.frame, offset: 5) } else { - if !answerCallButton.hidden { - answerCallButton.frame = CGRectMake((self.view.width - 72) / 2, self.view.height - 72 - 49, 72, 72) + if !answerCallButton.isHidden { + answerCallButton.frame = CGRect(x: (self.view.width - 72) / 2, y: self.view.height - 72 - 49, width: 72, height: 72) answerCallButtonText.under(answerCallButton.frame, offset: 5) } - if !declineCallButton.hidden { - declineCallButton.frame = CGRectMake((self.view.width - 72) / 2, self.view.height - 72 - 49, 72, 72) + if !declineCallButton.isHidden { + // declineCallButton.frame = CGRectMake((self.view.width - 72) / 2, self.view.height - 72 - 49, 72, 72) + declineCallButton.frame = CGRect(x: self.view.width - 72 - 25, y: self.view.height - 72 - 49, width: 72, height: 72) declineCallButtonText.under(declineCallButton.frame, offset: 5) } } } } - private func layoutVideo() { + fileprivate func layoutVideo() { if self.remoteVideoSize == nil { - remoteView.frame = CGRectMake(0, 0, self.view.width, self.view.height) + remoteView.frame = CGRect(x: 0, y: 0, width: self.view.width, height: self.view.height) } else { - remoteView.frame = AVMakeRectWithAspectRatioInsideRect(remoteVideoSize, view.bounds) + remoteView.frame = AVMakeRect(aspectRatio: remoteVideoSize, insideRect: view.bounds) } if self.localVideoSize == nil { - localView.frame = CGRectMake(self.view.width - 100 - 10, 10, 100, 120) + localView.frame = CGRect(x: self.view.width - 100 - 10, y: 10, width: 100, height: 120) } else { - let rect = AVMakeRectWithAspectRatioInsideRect(localVideoSize, CGRectMake(0, 0, 120, 120)) - localView.frame = CGRectMake(self.view.width - rect.width - 10, 10, rect.width, rect.height) + let rect = AVMakeRect(aspectRatio: localVideoSize, insideRect: CGRect(x: 0, y: 0, width: 120, height: 120)) + localView.frame = CGRect(x: self.view.width - rect.width - 10, y: 10, width: rect.width, height: rect.height) } } - private func CGSizeAspectFit(aspectRatio:CGSize, boundingSize:CGSize) -> CGSize { + fileprivate func CGSizeAspectFit(_ aspectRatio:CGSize, boundingSize:CGSize) -> CGSize { var aspectFitSize = boundingSize let mW = boundingSize.width / aspectRatio.width let mH = boundingSize.height / aspectRatio.height @@ -219,7 +225,7 @@ public class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { return aspectFitSize } - private func CGSizeAspectFill(aspectRatio:CGSize, minimumSize:CGSize) -> CGSize { + fileprivate func CGSizeAspectFill(_ aspectRatio:CGSize, minimumSize:CGSize) -> CGSize { var aspectFillSize = minimumSize let mW = minimumSize.width / aspectRatio.width let mH = minimumSize.height / aspectRatio.height @@ -234,83 +240,83 @@ public class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { return aspectFillSize } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // UI Configuration - UIApplication.sharedApplication().setStatusBarStyle(UIStatusBarStyle.LightContent, animated: true) + UIApplication.shared.setStatusBarStyle(UIStatusBarStyle.lightContent, animated: true) - UIDevice.currentDevice().proximityMonitoringEnabled = true + UIDevice.current.isProximityMonitoringEnabled = true // // Binding State // - binder.bind(call.isAudioEnabled) { (value: JavaLangBoolean!) -> () in - self.muteButton.filled = !value.booleanValue() + binder.bind(call.isAudioEnabled) { (value: JavaLangBoolean?) -> () in + self.muteButton.filled = !value!.booleanValue() } - binder.bind(call.state) { (value: ACCallState!) -> () in - if (ACCallState_Enum.RINGING == value.toNSEnum()) { + binder.bind(call.state) { (value: ACCallState?) -> () in + if (ACCallState_Enum.RINGING == value!.toNSEnum()) { if (self.call.isOutgoing) { self.muteButton.showViewAnimated() - self.videoButton.showViewAnimated() + // self.videoButton.showViewAnimated() - self.answerCallButton.hidden = true - self.answerCallButtonText.hidden = true - self.declineCallButton.hidden = false - self.declineCallButtonText.hidden = true + self.answerCallButton.isHidden = true + self.answerCallButtonText.isHidden = true + self.declineCallButton.isHidden = false + self.declineCallButtonText.isHidden = true self.callState.text = AALocalized("CallStateRinging") } else { - self.answerCallButton.hidden = false - self.answerCallButtonText.hidden = false - self.declineCallButton.hidden = false - self.declineCallButtonText.hidden = false + self.answerCallButton.isHidden = false + self.answerCallButtonText.isHidden = false + self.declineCallButton.isHidden = false + self.declineCallButtonText.isHidden = false self.callState.text = AALocalized("CallStateIncoming") } self.layoutButtons() - } else if (ACCallState_Enum.CONNECTING == value.toNSEnum()) { + } else if (ACCallState_Enum.CONNECTING == value!.toNSEnum()) { self.muteButton.showViewAnimated() - self.videoButton.showViewAnimated() + // self.videoButton.showViewAnimated() - self.answerCallButton.hidden = true - self.answerCallButtonText.hidden = true - self.declineCallButton.hidden = false - self.declineCallButtonText.hidden = true + self.answerCallButton.isHidden = true + self.answerCallButtonText.isHidden = true + self.declineCallButton.isHidden = false + self.declineCallButtonText.isHidden = true self.callState.text = AALocalized("CallStateConnecting") self.layoutButtons() - } else if (ACCallState_Enum.IN_PROGRESS == value.toNSEnum()) { + } else if (ACCallState_Enum.IN_PROGRESS == value!.toNSEnum()) { self.muteButton.showViewAnimated() - self.videoButton.showViewAnimated() + // self.videoButton.showViewAnimated() - self.answerCallButton.hidden = true - self.answerCallButtonText.hidden = true - self.declineCallButton.hidden = false - self.declineCallButtonText.hidden = true + self.answerCallButton.isHidden = true + self.answerCallButtonText.isHidden = true + self.declineCallButton.isHidden = false + self.declineCallButtonText.isHidden = true self.startTimer() self.layoutButtons() - } else if (ACCallState_Enum.ENDED == value.toNSEnum()) { + } else if (ACCallState_Enum.ENDED == value!.toNSEnum()) { self.muteButton.hideViewAnimated() - self.videoButton.hideViewAnimated() + // self.videoButton.hideViewAnimated() - self.answerCallButton.hidden = true - self.answerCallButtonText.hidden = true - self.declineCallButton.hidden = true - self.declineCallButtonText.hidden = true + self.answerCallButton.isHidden = true + self.answerCallButtonText.isHidden = true + self.declineCallButton.isHidden = true + self.declineCallButtonText.isHidden = true self.stopTimer() @@ -319,7 +325,7 @@ public class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { if (!self.isScheduledDispose) { self.isScheduledDispose = true dispatchAfterOnUi(0.8) { - self.dismiss() + self.dismissController() } } } else { @@ -334,18 +340,18 @@ public class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { if (call.peer.peerType.toNSEnum() == ACPeerType_Enum.PRIVATE) { let user = Actor.getUserWithUid(call.peer.peerId) - binder.bind(user.getNameModel(), closure: { (value: String!) -> () in + binder.bind(user.getNameModel(), closure: { (value: String?) -> () in self.peerTitle.text = value }) - binder.bind(user.getAvatarModel(), closure: { (value: ACAvatar!) -> () in + binder.bind(user.getAvatarModel(), closure: { (value: ACAvatar?) -> () in self.senderAvatar.bind(user.getNameModel().get(), id: Int(user.getId()), avatar: value) }) } else if (call.peer.peerType.toNSEnum() == ACPeerType_Enum.GROUP) { let group = Actor.getGroupWithGid(call.peer.peerId) - binder.bind(group.getNameModel(), closure: { (value: String!) -> () in + binder.bind(group.getNameModel(), closure: { (value: String?) -> () in self.peerTitle.text = value }) - binder.bind(group.getAvatarModel(), closure: { (value: ACAvatar!) -> () in + binder.bind(group.getAvatarModel(), closure: { (value: ACAvatar?) -> () in self.senderAvatar.bind(group.getNameModel().get(), id: Int(group.getId()), avatar: value) }) } @@ -359,29 +365,29 @@ public class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { if call.peer.isPrivate { // Bind Video Button - binder.bind(call.isVideoEnabled) { (value: JavaLangBoolean!) -> () in - self.videoButton.filled = value.booleanValue() - } +// binder.bind(call.isVideoEnabled) { (value: JavaLangBoolean!) -> () in +// self.videoButton.filled = value.booleanValue() +// } // Local Video can be only one, so we can just keep active track reference and handle changes - binder.bind(call.ownVideoTracks, closure: { (videoTracks: ACArrayListMediaTrack!) in + binder.bind(call.ownVideoTracks, closure: { (videoTracks: ACArrayListMediaTrack?) in var needUnbind = true - if videoTracks.size() > 0 { + if videoTracks!.size() > 0 { - let track = (videoTracks.getWithInt(0) as! CocoaVideoTrack).videoTrack + let track = (videoTracks!.getWith(0) as! CocoaVideoTrack).videoTrack if self.localVideoTrack != track { if self.localVideoTrack != nil { - self.localVideoTrack.removeRenderer(self.localView) + self.localVideoTrack.remove(self.localView) } self.localVideoTrack = track self.localView.showViewAnimated() - track.addRenderer(self.localView) + track.add(self.localView) } needUnbind = false } if needUnbind { if self.localVideoTrack != nil { - self.localVideoTrack.removeRenderer(self.localView) + self.localVideoTrack.remove(self.localView) self.localVideoTrack = nil } self.localView.hideViewAnimated() @@ -390,26 +396,26 @@ public class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { // In Private Calls we can have only one video stream from other side // We will assume only one active peer connection - binder.bind(call.theirVideoTracks, closure: { (videoTracks: ACArrayListMediaTrack!) in + binder.bind(call.theirVideoTracks, closure: { (videoTracks: ACArrayListMediaTrack?) in var needUnbind = true - if videoTracks.size() > 0 { + if videoTracks!.size() > 0 { - let track = (videoTracks.getWithInt(0) as! CocoaVideoTrack).videoTrack + let track = (videoTracks!.getWith(0) as! CocoaVideoTrack).videoTrack if self.remoteVideoTrack != track { if self.remoteVideoTrack != nil { - self.remoteVideoTrack.removeRenderer(self.remoteView) + self.remoteVideoTrack.remove(self.remoteView) } self.remoteVideoTrack = track self.remoteView.showViewAnimated() self.senderAvatar.hideViewAnimated() self.peerTitle.hideViewAnimated() - track.addRenderer(self.remoteView) + track.add(self.remoteView) } needUnbind = false } if needUnbind { if self.remoteVideoTrack != nil { - self.remoteVideoTrack.removeRenderer(self.remoteView) + self.remoteVideoTrack.remove(self.remoteView) self.remoteVideoTrack = nil } self.remoteView.hideViewAnimated() @@ -419,16 +425,16 @@ public class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { }) } else { - self.videoButton.filled = false - self.videoButton.enabled = false + // self.videoButton.filled = false + // self.videoButton.enabled = false } } - public override func viewWillDisappear(animated: Bool) { + open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - UIDevice.currentDevice().proximityMonitoringEnabled = false - UIApplication.sharedApplication().setStatusBarStyle(ActorSDK.sharedActor().style.vcStatusBarStyle, animated: true) + UIDevice.current.isProximityMonitoringEnabled = false + UIApplication.shared.setStatusBarStyle(ActorSDK.sharedActor().style.vcStatusBarStyle, animated: true) binder.unbindAll() } @@ -440,13 +446,13 @@ public class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { func startTimer() { timer?.invalidate() - timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: #selector(AACallViewController.updateTimer), userInfo: nil, repeats: true) + timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(AACallViewController.updateTimer), userInfo: nil, repeats: true) updateTimer() } func updateTimer() { if call.callStart > 0 { - let end = call.callEnd > 0 ? call.callEnd : jlong(NSDate().timeIntervalSince1970 * 1000) + let end = call.callEnd > 0 ? call.callEnd : jlong(Date().timeIntervalSince1970 * 1000) let secs = Int((end - call.callStart) / 1000) let seconds = secs % 60 @@ -463,4 +469,4 @@ public class AACallViewController: AAViewController, RTCEAGLVideoViewDelegate { timer = nil updateTimer() } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAComposeController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAComposeController.swift index 3149a5a6eb..f3be4ee748 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAComposeController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAComposeController.swift @@ -4,7 +4,7 @@ import UIKit -public class AAComposeController: AAContactsListContentController, AAContactsListContentControllerDelegate { +open class AAComposeController: AAContactsListContentController, AAContactsListContentControllerDelegate { public override init() { super.init() @@ -15,7 +15,7 @@ public class AAComposeController: AAContactsListContentController, AAContactsLis self.navigationItem.title = AALocalized("ComposeTitle") if AADevice.isiPad { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: UIBarButtonItemStyle.Plain, target: self, action: #selector(AAViewController.dismiss)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: UIBarButtonItemStyle.plain, target: self, action: #selector(self.dismissController)) } } @@ -23,7 +23,7 @@ public class AAComposeController: AAContactsListContentController, AAContactsLis fatalError("init(coder:) has not been implemented") } - public func willAddContacts(controller: AAContactsListContentController, section: AAManagedSection) { + open func willAddContacts(_ controller: AAContactsListContentController, section: AAManagedSection) { section.custom { (r:AACustomRow) -> () in r.height = 56 @@ -33,13 +33,27 @@ public class AAComposeController: AAContactsListContentController, AAContactsLis } r.selectAction = { () -> Bool in - self.navigateNext(AAGroupCreateViewController(), removeCurrent: true) + self.navigateNext(AAGroupCreateViewController(isChannel: false), removeCurrent: true) + return false + } + } + + section.custom { (r:AACustomRow) -> () in + + r.height = 56 + + r.closure = { (cell) -> () in + cell.bind("ic_create_channel", actionTitle: AALocalized("CreateChannel")) + } + + r.selectAction = { () -> Bool in + self.navigateNext(AAGroupCreateViewController(isChannel: true), removeCurrent: true) return false } } } - public func contactDidTap(controller: AAContactsListContentController, contact: ACContact) -> Bool { + open func contactDidTap(_ controller: AAContactsListContentController, contact: ACContact) -> Bool { if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer_userWithInt_(contact.uid)) { navigateDetail(customController) } else { @@ -47,4 +61,4 @@ public class AAComposeController: AAContactsListContentController, AAContactsLis } return false } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAGroupCreateViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAGroupCreateViewController.swift index cd16ca6f6b..ddfc337937 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAGroupCreateViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAGroupCreateViewController.swift @@ -4,31 +4,37 @@ import Foundation -public class AAGroupCreateViewController: AAViewController, UITextFieldDelegate { +open class AAGroupCreateViewController: AAViewController, UITextFieldDelegate { - private var addPhotoButton = UIButton() - private var avatarImageView = UIImageView() - private var hint = UILabel() + fileprivate let isChannel: Bool + fileprivate var addPhotoButton = UIButton() + fileprivate var avatarImageView = UIImageView() + fileprivate var hint = UILabel() - private var groupName = UITextField() - private var groupNameFieldSeparator = UIView() + fileprivate var groupName = UITextField() + fileprivate var groupNameFieldSeparator = UIView() - private var image: UIImage? + fileprivate var image: UIImage? - public override init(){ + public init(isChannel: Bool) { + self.isChannel = isChannel super.init(nibName: nil, bundle: nil) - self.navigationItem.title = AALocalized("CreateGroupTitle") + if isChannel { + self.navigationItem.title = AALocalized("CreateChannelTitle") + } else { + self.navigationItem.title = AALocalized("CreateGroupTitle") + } if AADevice.isiPad { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: UIBarButtonItemStyle.Plain, target: self, action: #selector(AAViewController.dismiss)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: UIBarButtonItemStyle.plain, target: self, action: #selector(self.dismissController)) } - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationNext"), style: UIBarButtonItemStyle.Plain, target: self, action: #selector(AAGroupCreateViewController.doNext)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationNext"), style: UIBarButtonItemStyle.done, target: self, action: #selector(AAGroupCreateViewController.doNext)) } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = appStyle.vcBgColor @@ -40,71 +46,80 @@ public class AAGroupCreateViewController: AAViewController, UITextFieldDelegate UIGraphicsBeginImageContextWithOptions(CGSize(width: 110, height: 110), false, 0.0); let context = UIGraphicsGetCurrentContext(); - CGContextSetFillColorWithColor(context, appStyle.composeAvatarBgColor.CGColor); - CGContextFillEllipseInRect(context, CGRectMake(0.0, 0.0, 110.0, 110.0)); - CGContextSetStrokeColorWithColor(context, appStyle.composeAvatarBorderColor.CGColor); - CGContextSetLineWidth(context, 1.0); - CGContextStrokeEllipseInRect(context, CGRectMake(0.5, 0.5, 109.0, 109.0)); + context?.setFillColor(appStyle.composeAvatarBgColor.cgColor); + context?.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: 110.0, height: 110.0)); + context?.setStrokeColor(appStyle.composeAvatarBorderColor.cgColor); + context?.setLineWidth(1.0); + context?.strokeEllipse(in: CGRect(x: 0.5, y: 0.5, width: 109.0, height: 109.0)); let buttonImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - addPhotoButton.exclusiveTouch = true - addPhotoButton.setBackgroundImage(buttonImage, forState: UIControlState.Normal) - addPhotoButton.addTarget(self, action: #selector(AAGroupCreateViewController.photoTap), forControlEvents: UIControlEvents.TouchUpInside) + addPhotoButton.isExclusiveTouch = true + addPhotoButton.setBackgroundImage(buttonImage, for: UIControlState()) + addPhotoButton.addTarget(self, action: #selector(AAGroupCreateViewController.photoTap), for: UIControlEvents.touchUpInside) let addPhotoLabelFirst = UILabel() addPhotoLabelFirst.text = AALocalized("ActionAddPhoto1") - addPhotoLabelFirst.font = UIFont.systemFontOfSize(15.0) - addPhotoLabelFirst.backgroundColor = UIColor.clearColor() + addPhotoLabelFirst.font = UIFont.systemFont(ofSize: 15.0) + addPhotoLabelFirst.backgroundColor = UIColor.clear addPhotoLabelFirst.textColor = appStyle.composeAvatarTextColor addPhotoLabelFirst.sizeToFit() let addPhotoLabelSecond = UILabel() addPhotoLabelSecond.text = AALocalized("ActionAddPhoto2") - addPhotoLabelSecond.font = UIFont.systemFontOfSize(15.0) - addPhotoLabelSecond.backgroundColor = UIColor.clearColor() + addPhotoLabelSecond.font = UIFont.systemFont(ofSize: 15.0) + addPhotoLabelSecond.backgroundColor = UIColor.clear addPhotoLabelSecond.textColor = appStyle.composeAvatarTextColor addPhotoLabelSecond.sizeToFit() addPhotoButton.addSubview(addPhotoLabelFirst) addPhotoButton.addSubview(addPhotoLabelSecond) - addPhotoLabelFirst.frame = CGRectIntegral(CGRectMake((80 - addPhotoLabelFirst.frame.size.width) / 2, 22, addPhotoLabelFirst.frame.size.width, addPhotoLabelFirst.frame.size.height)); - addPhotoLabelSecond.frame = CGRectIntegral(CGRectMake((80 - addPhotoLabelSecond.frame.size.width) / 2, 22 + 22, addPhotoLabelSecond.frame.size.width, addPhotoLabelSecond.frame.size.height)); + addPhotoLabelFirst.frame = CGRect(x: (80 - addPhotoLabelFirst.frame.size.width) / 2, y: 22, width: addPhotoLabelFirst.frame.size.width, height: addPhotoLabelFirst.frame.size.height).integral; + addPhotoLabelSecond.frame = CGRect(x: (80 - addPhotoLabelSecond.frame.size.width) / 2, y: 22 + 22, width: addPhotoLabelSecond.frame.size.width, height: addPhotoLabelSecond.frame.size.height).integral; groupName.backgroundColor = appStyle.vcBgColor groupName.textColor = ActorSDK.sharedActor().style.cellTextColor - groupName.font = UIFont.systemFontOfSize(20) - groupName.keyboardType = UIKeyboardType.Default - groupName.returnKeyType = UIReturnKeyType.Next - groupName.attributedPlaceholder = NSAttributedString(string: AALocalized("CreateGroupNamePlaceholder"), attributes: [NSForegroundColorAttributeName: ActorSDK.sharedActor().style.vcHintColor]) + groupName.font = UIFont.systemFont(ofSize: 20) + groupName.keyboardType = UIKeyboardType.default + groupName.returnKeyType = UIReturnKeyType.next + if isChannel { + groupName.attributedPlaceholder = NSAttributedString(string: AALocalized("CreateChannelNamePlaceholder"), attributes: [NSForegroundColorAttributeName: ActorSDK.sharedActor().style.vcHintColor]) + } else { + groupName.attributedPlaceholder = NSAttributedString(string: AALocalized("CreateGroupNamePlaceholder"), attributes: [NSForegroundColorAttributeName: ActorSDK.sharedActor().style.vcHintColor]) + } groupName.delegate = self - groupName.contentVerticalAlignment = UIControlContentVerticalAlignment.Center - groupName.autocapitalizationType = UITextAutocapitalizationType.Words - groupName.keyboardAppearance = appStyle.isDarkApp ? .Dark : .Light + groupName.contentVerticalAlignment = UIControlContentVerticalAlignment.center + groupName.autocapitalizationType = UITextAutocapitalizationType.words + groupName.keyboardAppearance = appStyle.isDarkApp ? .dark : .light groupNameFieldSeparator.backgroundColor = appStyle.vcSeparatorColor - hint.text = AALocalized("CreateGroupHint") - hint.font = UIFont.systemFontOfSize(15) - hint.lineBreakMode = .ByWordWrapping + if isChannel { + hint.text = AALocalized("CreateChannelHint") + } else { + hint.text = AALocalized("CreateGroupHint") + } + + hint.font = UIFont.systemFont(ofSize: 15) + hint.lineBreakMode = .byWordWrapping hint.numberOfLines = 0 hint.textColor = ActorSDK.sharedActor().style.vcHintColor } - public override func viewDidLayoutSubviews() { + open override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - avatarImageView.frame = CGRectMake(20, 20 + 66, 80, 80) + avatarImageView.frame = CGRect(x: 20, y: 20 + 66, width: 80, height: 80) addPhotoButton.frame = avatarImageView.frame - hint.frame = CGRectMake(120, 20 + 66, view.width - 140, 80) + hint.frame = CGRect(x: 120, y: 20 + 66, width: view.width - 140, height: 80) - groupName.frame = CGRectMake(20, 106 + 66, view.width - 20, 56.0) - groupNameFieldSeparator.frame = CGRectMake(20, 156 + 66, view.width - 20, 0.5) + groupName.frame = CGRect(x: 20, y: 106 + 66, width: view.width - 20, height: 56.0) + groupNameFieldSeparator.frame = CGRect(x: 20, y: 156 + 66, width: view.width - 20, height: 0.5) } - public func photoTap() { - let hasCamera = UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.Camera) + open func photoTap() { + let hasCamera = UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera) self.showActionSheet(hasCamera ? ["PhotoCamera", "PhotoLibrary"] : ["PhotoLibrary"], cancelButton: "AlertCancel", destructButton: self.avatarImageView.image != nil ? "PhotoRemove" : nil, @@ -124,30 +139,31 @@ public class AAGroupCreateViewController: AAViewController, UITextFieldDelegate }) } - public override func viewWillAppear(animated: Bool) { - super.viewWillAppear(animated) - + open override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) groupName.becomeFirstResponder() } - -// public override func viewDidAppear(animated: Bool) { -// super.viewDidAppear(animated) -// -// groupName.becomeFirstResponder() -// } - - public func textFieldShouldReturn(textField: UITextField) -> Bool { + + open func textFieldShouldReturn(_ textField: UITextField) -> Bool { doNext() return false } - public func doNext() { + open func doNext() { let title = groupName.text!.trim() if (title.length == 0) { shakeView(groupName, originalX: groupName.frame.origin.x) return } - navigateNext(GroupMembersController(title: title, image: image), removeCurrent: true) + groupName.resignFirstResponder() + + if isChannel { + executePromise(Actor.createChannel(withTitle: title, withAvatar: nil)).then({ (gid: JavaLangInteger!) in + self.navigateNext(AAGroupTypeViewController(gid: Int(gid.intValue()), isCreation: true), removeCurrent: true) + }) + } else { + navigateNext(GroupMembersController(title: title, image: image), removeCurrent: true) + } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAGroupMembersController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAGroupMembersController.swift index 888ced2978..0aa96b50ab 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAGroupMembersController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Compose/AAGroupMembersController.swift @@ -4,15 +4,15 @@ import UIKit -public class GroupMembersController: AAContactsListContentController, AAContactsListContentControllerDelegate, CLTokenInputViewDelegate { +open class GroupMembersController: AAContactsListContentController, AAContactsListContentControllerDelegate, CLTokenInputViewDelegate { - private var groupTitle: String! - private var groupImage: UIImage? + fileprivate var groupTitle: String! + fileprivate var groupImage: UIImage? - private var tokenView = CLTokenInputView() - private var tokenViewHeight: CGFloat = 48 + fileprivate var tokenView = CLTokenInputView() + fileprivate var tokenViewHeight: CGFloat = 48 - private var selected = [TokenRef]() + fileprivate var selected = [TokenRef]() public init(title: String, image: UIImage?) { super.init() @@ -25,17 +25,17 @@ public class GroupMembersController: AAContactsListContentController, AAContacts navigationItem.title = AALocalized("CreateGroupMembersTitle") if AADevice.isiPad { - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: UIBarButtonItemStyle.Plain, target: self, action: Selector("dismiss")) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: UIBarButtonItemStyle.plain, target: self, action: #selector(self.dismissController)) } - navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationDone"), style: UIBarButtonItemStyle.Done, target: self, action: #selector(GroupMembersController.doNext)) + navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationDone"), style: UIBarButtonItemStyle.done, target: self, action: #selector(GroupMembersController.doNext)) } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public override func tableDidLoad() { + open override func tableDidLoad() { super.tableDidLoad() tokenView.delegate = self @@ -52,22 +52,22 @@ public class GroupMembersController: AAContactsListContentController, AAContacts self.view.addSubview(tokenView) - tableView.keyboardDismissMode = UIScrollViewKeyboardDismissMode.OnDrag + tableView.keyboardDismissMode = UIScrollViewKeyboardDismissMode.onDrag } - public func contactDidTap(controller: AAContactsListContentController, contact: ACContact) -> Bool { + open func contactDidTap(_ controller: AAContactsListContentController, contact: ACContact) -> Bool { for i in 0.. Void in - let gid = (val as! JavaLangInteger).intValue + executePromise(Actor.createGroup(withTitle: groupTitle, withAvatar: nil, withUids: res)).then { (res: JavaLangInteger!) in + let gid = res.int32Value if self.groupImage != nil { Actor.changeGroupAvatar(gid, image: self.groupImage!) } - if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer.groupWithInt(gid)) { + if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer.group(with: gid)) { self.navigateDetail(customController) } else { - self.navigateDetail(ConversationViewController(peer: ACPeer.groupWithInt(gid))) + self.navigateDetail(ConversationViewController(peer: ACPeer.group(with: gid))) } - self.dismiss() + self.dismissController() } } // Handling token input updates - public func tokenInputView(view: CLTokenInputView, didChangeText text: String?) { + open func tokenInputView(_ view: CLTokenInputView, didChangeText text: String?) { contactRows.filter(text!) } - public func tokenInputView(view: CLTokenInputView, didChangeHeightTo height: CGFloat) { + open func tokenInputView(_ view: CLTokenInputView, didChangeHeightTo height: CGFloat) { tokenViewHeight = height self.view.setNeedsLayout() } - public func tokenInputView(view: CLTokenInputView, didRemoveToken token: CLToken) { + open func tokenInputView(_ view: CLTokenInputView, didRemove token: CLToken) { for i in 0..(lhs: T?, rhs: T?) -> Bool { + switch (lhs, rhs) { + case let (l?, r?): + return l < r + case (nil, _?): + return true + default: + return false + } +} -public class AAContactsViewController: AAContactsListContentController, AAContactsListContentControllerDelegate, UIAlertViewDelegate, MFMessageComposeViewControllerDelegate, MFMailComposeViewControllerDelegate { +fileprivate func > (lhs: T?, rhs: T?) -> Bool { + switch (lhs, rhs) { + case let (l?, r?): + return l > r + default: + return rhs < lhs + } +} + + +open class AAContactsViewController: AAContactsListContentController, AAContactsListContentControllerDelegate, UIAlertViewDelegate, MFMessageComposeViewControllerDelegate, MFMailComposeViewControllerDelegate { var inviteText: String { get { @@ -19,13 +39,13 @@ public class AAContactsViewController: AAContactsListContentController, AAContac public override init() { super.init() - content = ACAllEvents_Main.CONTACTS() + content = ACAllEvents_Main.contacts() tabBarItem = UITabBarItem(title: "TabPeople", img: "TabIconContacts", selImage: "TabIconContactsHighlighted") navigationItem.title = AALocalized("TabPeople") - navigationItem.backBarButtonItem = UIBarButtonItem(title: AALocalized("ContactsBack"), style: UIBarButtonItemStyle.Plain, target: nil, action: nil) - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Add, target: self, action: #selector(AAContactsViewController.findContact)) + navigationItem.backBarButtonItem = UIBarButtonItem(title: AALocalized("ContactsBack"), style: UIBarButtonItemStyle.plain, target: nil, action: nil) + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.add, target: self, action: #selector(AAContactsViewController.findContact)) delegate = self } @@ -34,7 +54,7 @@ public class AAContactsViewController: AAContactsListContentController, AAContac fatalError("init(coder:) has not been implemented") } - public func contactDidTap(controller: AAContactsListContentController, contact: ACContact) -> Bool { + open func contactDidTap(_ controller: AAContactsListContentController, contact: ACContact) -> Bool { if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer_userWithInt_(contact.uid)) { navigateDetail(customController) @@ -45,7 +65,7 @@ public class AAContactsViewController: AAContactsListContentController, AAContac return true } - public func willAddContacts(controller: AAContactsListContentController, section: AAManagedSection) { + open func willAddContacts(_ controller: AAContactsListContentController, section: AAManagedSection) { section.custom { (r: AACustomRow) -> () in @@ -85,39 +105,39 @@ public class AAContactsViewController: AAContactsListContentController, AAContac } } - if SLComposeViewController.isAvailableForServiceType(SLServiceTypeTencentWeibo) { + if SLComposeViewController.isAvailable(forServiceType: SLServiceTypeTencentWeibo) { builder.add("Tencent Weibo") { () -> () in - let vc = SLComposeViewController(forServiceType: SLServiceTypeTencentWeibo) + let vc = SLComposeViewController(forServiceType: SLServiceTypeTencentWeibo)! vc.setInitialText(self.inviteText) - self.presentViewController(vc, animated: true, completion: nil) + self.present(vc, animated: true, completion: nil) } } - if SLComposeViewController.isAvailableForServiceType(SLServiceTypeSinaWeibo) { + if SLComposeViewController.isAvailable(forServiceType: SLServiceTypeSinaWeibo) { builder.add("Sina Weibo") { () -> () in - let vc = SLComposeViewController(forServiceType: SLServiceTypeSinaWeibo) + let vc = SLComposeViewController(forServiceType: SLServiceTypeSinaWeibo)! vc.setInitialText(self.inviteText) - self.presentViewController(vc, animated: true, completion: nil) + self.present(vc, animated: true, completion: nil) } } - if SLComposeViewController.isAvailableForServiceType(SLServiceTypeTwitter) { + if SLComposeViewController.isAvailable(forServiceType: SLServiceTypeTwitter) { builder.add("Twitter") { () -> () in - let vc = SLComposeViewController(forServiceType: SLServiceTypeTwitter) + let vc = SLComposeViewController(forServiceType: SLServiceTypeTwitter)! vc.setInitialText(self.inviteText) - self.presentViewController(vc, animated: true, completion: nil) + self.present(vc, animated: true, completion: nil) } } - if SLComposeViewController.isAvailableForServiceType(SLServiceTypeFacebook) { + if SLComposeViewController.isAvailable(forServiceType: SLServiceTypeFacebook) { builder.add("Facebook") { () -> () in - let vc = SLComposeViewController(forServiceType: SLServiceTypeFacebook) - vc.addURL(NSURL(string: ActorSDK.sharedActor().inviteUrl)) - self.presentViewController(vc, animated: true, completion: nil) + let vc = SLComposeViewController(forServiceType: SLServiceTypeFacebook)! + vc.add(URL(string: ActorSDK.sharedActor().inviteUrl)) + self.present(vc, animated: true, completion: nil) } } - let view = self.tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 1, inSection: 0))!.contentView + let view = self.tableView.cellForRow(at: IndexPath(row: 1, section: 0))!.contentView self.showActionSheet(builder.items, cancelButton: "AlertCancel", destructButton: nil, sourceView: view, sourceRect: view.bounds, tapClosure: builder.tapClosure) @@ -128,7 +148,7 @@ public class AAContactsViewController: AAContactsListContentController, AAContac // Searching for contact - public func findContact() { + open func findContact() { startEditField { (c) -> () in c.title = "FindTitle" @@ -137,9 +157,9 @@ public class AAContactsViewController: AAContactsListContentController, AAContac c.hint = "FindHint" c.fieldHint = "FindFieldHint" - c.fieldAutocapitalizationType = .None - c.fieldAutocorrectionType = .No - c.fieldReturnKey = .Search + c.fieldAutocapitalizationType = .none + c.fieldAutocorrectionType = .no + c.fieldReturnKey = .search c.didDoneTap = { (t, c) -> () in @@ -147,31 +167,31 @@ public class AAContactsViewController: AAContactsListContentController, AAContac return } - self.executeSafeOnlySuccess(Actor.findUsersCommandWithQuery(t), successBlock: { (val) -> Void in + self.executeSafeOnlySuccess(Actor.findUsersCommand(withQuery: t), successBlock: { (val) -> Void in var user: ACUserVM? = nil if let users = val as? IOSObjectArray { if Int(users.length()) > 0 { - if let tempUser = users.objectAtIndex(0) as? ACUserVM { + if let tempUser = users.object(at: 0) as? ACUserVM { user = tempUser } } } if user != nil { - c.execute(Actor.addContactCommandWithUid(user!.getId())!, successBlock: { (val) -> Void in + c.execute(Actor.addContactCommand(withUid: user!.getId())!, successBlock: { (val) -> Void in if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer_userWithInt_(user!.getId())) { self.navigateDetail(customController) } else { self.navigateDetail(ConversationViewController(peer: ACPeer_userWithInt_(user!.getId()))) } - c.dismiss() + c.dismissController() }, failureBlock: { (val) -> Void in if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer_userWithInt_(user!.getId())) { self.navigateDetail(customController) } else { self.navigateDetail(ConversationViewController(peer: ACPeer_userWithInt_(user!.getId()))) } - c.dismiss() + c.dismissController() }) } else { c.alertUser("FindNotFound") @@ -181,24 +201,24 @@ public class AAContactsViewController: AAContactsListContentController, AAContac } } - public func alertView(alertView: UIAlertView, clickedButtonAtIndex buttonIndex: Int) { + open func alertView(_ alertView: UIAlertView, clickedButtonAt buttonIndex: Int) { if buttonIndex == 1 { - let textField = alertView.textFieldAtIndex(0)! + let textField = alertView.textField(at: 0)! if textField.text?.length > 0 { - execute(Actor.findUsersCommandWithQuery(textField.text), successBlock: { (val) -> () in + execute(Actor.findUsersCommand(withQuery: textField.text), successBlock: { (val) -> () in var user: ACUserVM? user = val as? ACUserVM if user == nil { if let users = val as? IOSObjectArray { if Int(users.length()) > 0 { - if let tempUser = users.objectAtIndex(0) as? ACUserVM { + if let tempUser = users.object(at: 0) as? ACUserVM { user = tempUser } } } } if user != nil { - self.execute(Actor.addContactCommandWithUid(user!.getId())!, successBlock: { (val) -> () in + self.execute(Actor.addContactCommand(withUid: user!.getId())!, successBlock: { (val) -> () in if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer_userWithInt_(user!.getId())) { self.navigateDetail(customController) } else { @@ -219,7 +239,7 @@ public class AAContactsViewController: AAContactsListContentController, AAContac // Email Invitation - public func showEmailInvitation(recipients: [String]?) { + open func showEmailInvitation(_ recipients: [String]?) { if MFMailComposeViewController.canSendMail() { let messageComposeController = MFMailComposeViewController() @@ -231,30 +251,30 @@ public class AAContactsViewController: AAContactsListContentController, AAContac // TODO: Replace with bigger text messageComposeController.setMessageBody(inviteText, isHTML: false) messageComposeController.setToRecipients(recipients) - presentViewController(messageComposeController, animated: true, completion: nil) + present(messageComposeController, animated: true, completion: nil) } } - public func mailComposeController(controller: MFMailComposeViewController, didFinishWithResult result: MFMailComposeResult, error: NSError?) { - controller.dismissViewControllerAnimated(true, completion: nil) + open func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + controller.dismiss(animated: true, completion: nil) } // SMS Invitation - public func showSmsInvitation() { + open func showSmsInvitation() { self.showSmsInvitation(nil) } - public func showSmsInvitation(recipients: [String]?) { + open func showSmsInvitation(_ recipients: [String]?) { if MFMessageComposeViewController.canSendText() { let messageComposeController = MFMessageComposeViewController() messageComposeController.messageComposeDelegate = self messageComposeController.body = inviteText messageComposeController.recipients = recipients - presentViewController(messageComposeController, animated: true, completion: nil) + present(messageComposeController, animated: true, completion: nil) } } - @objc public func messageComposeViewController(controller: MFMessageComposeViewController, didFinishWithResult result: MessageComposeResult) { - controller.dismissViewControllerAnimated(true, completion: nil) + @objc open func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) { + controller.dismiss(animated: true, completion: nil) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Contacts List/AAContactsListContentController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Contacts List/AAContactsListContentController.swift index 0a12abb738..61119c1e3e 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Contacts List/AAContactsListContentController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Contacts List/AAContactsListContentController.swift @@ -4,22 +4,22 @@ import Foundation -public class AAContactsListContentController: AAContentTableController { +open class AAContactsListContentController: AAContentTableController { - public var delegate: AAContactsListContentControllerDelegate? - public var isSearchAutoHide: Bool = true - public var contactRows: AABindedRows! - public var searchEnabled: Bool = true + open var delegate: AAContactsListContentControllerDelegate? + open var isSearchAutoHide: Bool = true + open var contactRows: AABindedRows! + open var searchEnabled: Bool = true public init() { - super.init(style: .Plain) + super.init(style: .plain) } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public override func tableDidLoad() { + open override func tableDidLoad() { if searchEnabled { search(AAContactCell.self) { (s) -> () in @@ -78,7 +78,7 @@ public class AAContactsListContentController: AAContentTableController { action2Selector: nil) } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) binder.bind(Actor.getAppState().isContactsEmpty, closure: { (value: Any?) -> () in @@ -91,4 +91,4 @@ public class AAContactsListContentController: AAContentTableController { } }) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Contacts List/AAContactsListContentControllerDelegate.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Contacts List/AAContactsListContentControllerDelegate.swift index 2af5b3573c..10235d0790 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Contacts List/AAContactsListContentControllerDelegate.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Contacts List/AAContactsListContentControllerDelegate.swift @@ -6,27 +6,27 @@ import Foundation public protocol AAContactsListContentControllerDelegate { - func willAddContacts(controller: AAContactsListContentController, section: AAManagedSection) + func willAddContacts(_ controller: AAContactsListContentController, section: AAManagedSection) - func didAddContacts(controller: AAContactsListContentController, section: AAManagedSection) + func didAddContacts(_ controller: AAContactsListContentController, section: AAManagedSection) - func contactDidTap(controller: AAContactsListContentController, contact: ACContact) -> Bool + func contactDidTap(_ controller: AAContactsListContentController, contact: ACContact) -> Bool - func contactDidBind(controller: AAContactsListContentController, contact: ACContact, cell: AAContactCell) + func contactDidBind(_ controller: AAContactsListContentController, contact: ACContact, cell: AAContactCell) } public extension AAContactsListContentControllerDelegate { - public func willAddContacts(controller: AAContactsListContentController, section: AAManagedSection) { + public func willAddContacts(_ controller: AAContactsListContentController, section: AAManagedSection) { // Do Nothing } - public func didAddContacts(controller: AAContactsListContentController, section: AAManagedSection) { + public func didAddContacts(_ controller: AAContactsListContentController, section: AAManagedSection) { // Do Nothing } - public func contactDidBind(controller: AAContactsListContentController, contact: ACContact, cell: AAContactCell) { + public func contactDidBind(_ controller: AAContactsListContentController, contact: ACContact, cell: AAContactCell) { // Do Nothing } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Contacts List/Cells/AAContactActionCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Contacts List/Cells/AAContactActionCell.swift index 0f8db0723a..1ffd7a496c 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Contacts List/Cells/AAContactActionCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Contacts List/Cells/AAContactActionCell.swift @@ -4,18 +4,18 @@ import Foundation -public class AAContactActionCell: AATableViewCell { +open class AAContactActionCell: AATableViewCell { - public let titleView = YYLabel() - public let iconView = UIImageView() + open let titleView = YYLabel() + open let iconView = UIImageView() public override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - titleView.font = UIFont.systemFontOfSize(18) + titleView.font = UIFont.systemFont(ofSize: 18) titleView.textColor = ActorSDK.sharedActor().style.cellTintColor titleView.displaysAsynchronously = true - iconView.contentMode = UIViewContentMode.Center + iconView.contentMode = UIViewContentMode.center self.contentView.addSubview(titleView) self.contentView.addSubview(iconView) } @@ -24,15 +24,15 @@ public class AAContactActionCell: AATableViewCell { fatalError("init(coder:) has not been implemented") } - public func bind(icon: String, actionTitle: String) { + open func bind(_ icon: String, actionTitle: String) { titleView.text = actionTitle iconView.image = UIImage.bundled(icon)?.tintImage(ActorSDK.sharedActor().style.cellTintColor) } - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() let width = self.contentView.frame.width; - iconView.frame = CGRectMake(30, 8, 40, 40); - titleView.frame = CGRectMake(80, 8, width - 80 - 14, 40); + iconView.frame = CGRect(x: 30, y: 8, width: 40, height: 40); + titleView.frame = CGRect(x: 80, y: 8, width: width - 80 - 14, height: 40); } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Contacts List/Cells/AAContactCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Contacts List/Cells/AAContactCell.swift index 86acf27180..e7ba4f02ad 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Contacts List/Cells/AAContactCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Contacts List/Cells/AAContactCell.swift @@ -5,31 +5,31 @@ import Foundation import UIKit -public class AAContactCell : AATableViewCell, AABindedCell, AABindedSearchCell { +open class AAContactCell : AATableViewCell, AABindedCell, AABindedSearchCell { public typealias BindData = ACContact - public static func bindedCellHeight(table: AAManagedTable, item: BindData) -> CGFloat { + open static func bindedCellHeight(_ table: AAManagedTable, item: BindData) -> CGFloat { return 56 } - public static func bindedCellHeight(item: BindData) -> CGFloat { + open static func bindedCellHeight(_ item: BindData) -> CGFloat { return 56 } - public let avatarView = AAAvatarView() - public let shortNameView = YYLabel() - public let titleView = YYLabel() + open let avatarView = AAAvatarView() + open let shortNameView = YYLabel() + open let titleView = YYLabel() public override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - titleView.font = UIFont.systemFontOfSize(18) + titleView.font = UIFont.systemFont(ofSize: 18) titleView.textColor = appStyle.contactTitleColor titleView.displaysAsynchronously = true - shortNameView.font = UIFont.boldSystemFontOfSize(18) - shortNameView.textAlignment = NSTextAlignment.Center + shortNameView.font = UIFont.boldSystemFont(ofSize: 18) + shortNameView.textAlignment = NSTextAlignment.center shortNameView.textColor = appStyle.contactTitleColor shortNameView.displaysAsynchronously = true @@ -42,23 +42,23 @@ public class AAContactCell : AATableViewCell, AABindedCell, AABindedSearchCell { fatalError("init(coder:) has not been implemented") } - public func bind(item: ACContact, search: String?) { + open func bind(_ item: ACContact, search: String?) { bind(item) } - public func bind(item: ACContact, table: AAManagedTable, index: Int, totalCount: Int) { + open func bind(_ item: ACContact, table: AAManagedTable, index: Int, totalCount: Int) { bind(item) } - func bind(item: ACContact) { + func bind(_ item: ACContact) { avatarView.bind(item.name, id: Int(item.uid), avatar: item.avatar); titleView.text = item.name; - shortNameView.hidden = true + shortNameView.isHidden = true } - func bindDisabled(disabled: Bool) { + func bindDisabled(_ disabled: Bool) { if disabled { titleView.alpha = 0.5 avatarView.alpha = 0.5 @@ -68,11 +68,11 @@ public class AAContactCell : AATableViewCell, AABindedCell, AABindedSearchCell { } } - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() let width = self.contentView.frame.width; - shortNameView.frame = CGRectMake(0, 8, 30, 40); - avatarView.frame = CGRectMake(30, 8, 44, 44); - titleView.frame = CGRectMake(80, 8, width - 80 - 14, 40); + shortNameView.frame = CGRect(x: 0, y: 8, width: 30, height: 40); + avatarView.frame = CGRect(x: 30, y: 8, width: 44, height: 44); + titleView.frame = CGRect(x: 80, y: 8, width: width - 80 - 14, height: 40); } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/AABubbles.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/AABubbles.swift index b7f62aefe5..29a5cd81dc 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/AABubbles.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/AABubbles.swift @@ -28,7 +28,7 @@ class AABubbles { static var layouters: [AABubbleLayouter] = builtInLayouters - class func layouterForMessage(message: ACMessage) -> AABubbleLayouter { + class func layouterForMessage(_ message: ACMessage) -> AABubbleLayouter { for layouter in layouters { if layouter.isSuitable(message) { return layouter @@ -37,7 +37,7 @@ class AABubbles { return textLayouter } - class func buildLayout(peer: ACPeer, message: ACMessage) -> AACellLayout { + class func buildLayout(_ peer: ACPeer, message: ACMessage) -> AACellLayout { for layouter in layouters { if (layouter.isSuitable(message)) { @@ -48,4 +48,4 @@ class AABubbles { return textLayouter.buildLayout(peer, message: message) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/AAConversationContentController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/AAConversationContentController.swift index 0d2cf82b08..158e030240 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/AAConversationContentController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/AAConversationContentController.swift @@ -8,47 +8,47 @@ import MediaPlayer import AVKit import AVFoundation -public class AAConversationContentController: SLKTextViewController, ARDisplayList_AppleChangeListener { +open class AAConversationContentController: SLKTextViewController, ARDisplayList_AppleChangeListener { - public let peer: ACPeer + open let peer: ACPeer - private let delayLoad = false + fileprivate let delayLoad = false - private let binder = AABinder() + fileprivate let binder = AABinder() - private var displayList: ARBindedDisplayList! - private var isStarted: Bool = AADevice.isiPad - private var isVisible: Bool = false - private var isLoaded: Bool = false - private var isLoadedAfter: Bool = false - private var unreadIndex: Int? = nil - private let collectionViewLayout = AAMessagesFlowLayout() - private var prevCount: Int = 0 - private var unreadMessageId: jlong = 0 + fileprivate var displayList: ARBindedDisplayList! + fileprivate var isStarted: Bool = AADevice.isiPad + fileprivate var isVisible: Bool = false + fileprivate var isLoaded: Bool = false + fileprivate var isLoadedAfter: Bool = false + fileprivate var unreadIndex: Int? = nil + fileprivate let collectionViewLayout = AAMessagesFlowLayout() + fileprivate var prevCount: Int = 0 + fileprivate var unreadMessageId: jlong = 0 - private var isUpdating: Bool = false - private var isBinded: Bool = false - private var pendingUpdates = [ARAppleListUpdate]() - private var readDate: jlong = 0 - private var receiveDate: jlong = 0 + fileprivate var isUpdating: Bool = false + fileprivate var isBinded: Bool = false + fileprivate var pendingUpdates = [ARAppleListUpdate]() + fileprivate var readDate: jlong = 0 + fileprivate var receiveDate: jlong = 0 // Audio notes - public var voicePlayer : AAModernConversationAudioPlayer! - public var voiceContext : AAModernViewInlineMediaContext! + open var voicePlayer : AAModernConversationAudioPlayer! + open var voiceContext : AAModernViewInlineMediaContext! - public var currentAudioFileId: jlong = 0 - public var voicesCache: Dictionary = Dictionary() + open var currentAudioFileId: jlong = 0 + open var voicesCache: Dictionary = Dictionary() public init(peer: ACPeer) { self.peer = peer super.init(collectionViewLayout: collectionViewLayout) - self.collectionView.backgroundColor = UIColor.clearColor() + self.collectionView.backgroundColor = UIColor.clear self.collectionView.alwaysBounceVertical = true for layout in AABubbles.layouters { - self.collectionView.registerClass(layout.cellClass(), forCellWithReuseIdentifier: layout.cellReuseId()) + self.collectionView.register(layout.cellClass(), forCellWithReuseIdentifier: layout.cellReuseId()) } } @@ -58,7 +58,7 @@ public class AAConversationContentController: SLKTextViewController, ARDisplayLi // Controller and UI lifecycle - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() if (self.displayList == nil) { @@ -66,7 +66,7 @@ public class AAConversationContentController: SLKTextViewController, ARDisplayLi } } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) isVisible = true @@ -83,7 +83,7 @@ public class AAConversationContentController: SLKTextViewController, ARDisplayLi self.collectionView.alpha = 0 } - dispatch_async(dispatch_get_main_queue(),{ + DispatchQueue.main.async(execute: { // What if controller is already closed? if (!self.isVisible) { return @@ -91,23 +91,23 @@ public class AAConversationContentController: SLKTextViewController, ARDisplayLi self.isStarted = true - UIView.animateWithDuration(0.6, animations: { () -> Void in self.collectionView.alpha = 1 }, completion: { (comp) -> Void in }) + UIView.animate(withDuration: 0.6, animations: { () -> Void in self.collectionView.alpha = 1 }, completion: { (comp) -> Void in }) self.tryBind() }) } else { self.isStarted = true - UIView.animateWithDuration(0.6, animations: { () -> Void in self.collectionView.alpha = 1 }, completion: { (comp) -> Void in }) + UIView.animate(withDuration: 0.6, animations: { () -> Void in self.collectionView.alpha = 1 }, completion: { (comp) -> Void in }) tryBind() } } - private func tryBind() { + fileprivate func tryBind() { if !self.isBinded { - self.binder.bind(Actor.getConversationVMWithACPeer(peer).getReadDate()) { (val: JavaLangLong!) in + self.binder.bind(Actor.getConversationVM(with: peer).getReadDate()) { (val: JavaLangLong?) in let nReadDate = val!.longLongValue() let oReadDate = self.readDate @@ -117,7 +117,7 @@ public class AAConversationContentController: SLKTextViewController, ARDisplayLi self.messageStatesUpdated(oReadDate, end: nReadDate) } } - self.binder.bind(Actor.getConversationVMWithACPeer(peer).getReceiveDate()) { (val: JavaLangLong!) in + self.binder.bind(Actor.getConversationVM(with: peer).getReceiveDate()) { (val: JavaLangLong?) in let nReceiveDate = val!.longLongValue() let oReceiveDate = self.receiveDate @@ -139,45 +139,45 @@ public class AAConversationContentController: SLKTextViewController, ARDisplayLi } - public func buildCell(collectionView: UICollectionView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { + open func buildCell(_ collectionView: UICollectionView, cellForRowAtIndexPath indexPath: IndexPath) -> UICollectionViewCell { let list = getProcessedList() - let layout = list!.layouts[indexPath.row] - let cell = collectionView.dequeueReusableCellWithReuseIdentifier(layout.layouter.cellReuseId(), forIndexPath: indexPath) + let layout = list!.layouts[(indexPath as NSIndexPath).row] + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: layout.layouter.cellReuseId(), for: indexPath) (cell as! AABubbleCell).setConfig(peer, controller: self) return cell } - public func bindCell(collectionView: UICollectionView, cellForRowAtIndexPath indexPath: NSIndexPath, cell: UICollectionViewCell) { + open func bindCell(_ collectionView: UICollectionView, cellForRowAtIndexPath indexPath: IndexPath, cell: UICollectionViewCell) { let list = getProcessedList() - let message = list!.items[indexPath.row] - let setting = list!.cellSettings[indexPath.row] - let layout = list!.layouts[indexPath.row] + let message = list!.items[(indexPath as NSIndexPath).row] + let setting = list!.cellSettings[(indexPath as NSIndexPath).row] + let layout = list!.layouts[(indexPath as NSIndexPath).row] let bubbleCell = (cell as! AABubbleCell) let isShowNewMessages = message.rid == unreadMessageId bubbleCell.performBind(message, receiveDate: receiveDate, readDate: readDate, setting: setting, isShowNewMessages: isShowNewMessages, layout: layout) } - public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets { + open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets { return UIEdgeInsetsMake(6, 0, 100, 0) } - public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat { + open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat { return 0 } - public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat { + open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat { return 0 } - public override func collectionView(collectionView: UICollectionView, canPerformAction action: Selector, forItemAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject!) -> Bool { + open override func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any!) -> Bool { return true } - public override func collectionView(collectionView: UICollectionView, shouldShowMenuForItemAtIndexPath indexPath: NSIndexPath) -> Bool { + open override func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool { return true } - public override func collectionView(collectionView: UICollectionView, performAction action: Selector, forItemAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject!) { + open override func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any!) { } @@ -192,28 +192,28 @@ public class AAConversationContentController: SLKTextViewController, ARDisplayLi return self.displayList.getProcessedList() as? AAPreprocessedList } - public override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + open override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return isStarted ? getCount() : 0 } - public override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { + open override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = buildCell(collectionView, cellForRowAtIndexPath: indexPath) bindCell(collectionView, cellForRowAtIndexPath: indexPath, cell: cell) - displayList.touchWithIndex(jint(indexPath.row)) + displayList.touch(with: jint((indexPath as NSIndexPath).row)) return cell } - public override func collectionView(collectionView: UICollectionView, didUnhighlightItemAtIndexPath indexPath: NSIndexPath) { - let cell = collectionView.cellForItemAtIndexPath(indexPath) as! AABubbleCell + open override func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) { + let cell = collectionView.cellForItem(at: indexPath) as! AABubbleCell cell.updateView() } - public override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { - let cell = collectionView.cellForItemAtIndexPath(indexPath) as! AABubbleCell + open override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let cell = collectionView.cellForItem(at: indexPath) as! AABubbleCell cell.updateView() } - public override func viewDidDisappear(animated: Bool) { + open override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) isVisible = false @@ -233,28 +233,28 @@ public class AAConversationContentController: SLKTextViewController, ARDisplayLi func displayListForController() -> ARBindedDisplayList { let res = Actor.getMessageDisplayList(peer) - if (res.getListProcessor() == nil) { - res.setListProcessor(AAListProcessor(peer: peer)) + if (res?.getProcessor() == nil) { + res?.setListProcessor(AAListProcessor(peer: peer)) } - return res + return res! } - public func objectAtIndexPath(indexPath: NSIndexPath) -> AnyObject? { - return objectAtIndex(indexPath.row) + open func objectAtIndexPath(_ indexPath: IndexPath) -> AnyObject? { + return objectAtIndex((indexPath as NSIndexPath).row) } - public func objectAtIndex(index: Int) -> AnyObject? { - return displayList.itemWithIndex(jint(index)) + open func objectAtIndex(_ index: Int) -> AnyObject? { + return displayList.item(with: jint(index)) as AnyObject? } - public func getCount() -> Int { + open func getCount() -> Int { if (isUpdating) { return self.prevCount } return Int(displayList.size()) } - public func onCollectionChangedWithChanges(modification: ARAppleListUpdate!) { + open func onCollectionChanged(withChanges modification: ARAppleListUpdate!) { // if isUpdating { // pendingUpdates.append(modification) @@ -284,34 +284,34 @@ public class AAConversationContentController: SLKTextViewController, ARDisplayLi // Removed rows if modification.removedCount() > 0 { - var rows = [NSIndexPath]() + var rows = [IndexPath]() for i in 0.. 0 { - var rows = [NSIndexPath]() + var rows = [IndexPath]() for i in 0.. 0 { for i in 0.. \(destRow)") } } @@ -364,25 +364,25 @@ public class AAConversationContentController: SLKTextViewController, ARDisplayLi } } - var forcedRows = [NSIndexPath]() - let visibleIndexes = self.collectionView.indexPathsForVisibleItems() + var forcedRows = [IndexPath]() + let visibleIndexes = self.collectionView.indexPathsForVisibleItems for ind in updated { - let indexPath = NSIndexPath(forRow: ind, inSection: 0) + let indexPath = IndexPath(row: ind, section: 0) if visibleIndexes.contains(indexPath) { - let cell = self.collectionView.cellForItemAtIndexPath(indexPath) + let cell = self.collectionView.cellForItem(at: indexPath) self.bindCell(self.collectionView, cellForRowAtIndexPath: indexPath, cell: cell!) } } for ind in updatedForce { - let indexPath = NSIndexPath(forRow: ind, inSection: 0) + let indexPath = IndexPath(row: ind, section: 0) forcedRows.append(indexPath) } if (forcedRows.count > 0) { self.collectionViewLayout.beginUpdates(false, list: list, unread: unreadMessageId) self.collectionView.performBatchUpdates({ () -> Void in - self.collectionView.reloadItemsAtIndexPaths(forcedRows) + self.collectionView.reloadItems(at: forcedRows) }, completion: nil) } @@ -394,23 +394,23 @@ public class AAConversationContentController: SLKTextViewController, ARDisplayLi } } - private func completeUpdates(modification: ARAppleListUpdate!) { + fileprivate func completeUpdates(_ modification: ARAppleListUpdate!) { } - private func messageStatesUpdated(start: jlong, end: jlong) { - let visibleIndexes = self.collectionView.indexPathsForVisibleItems() + fileprivate func messageStatesUpdated(_ start: jlong, end: jlong) { + let visibleIndexes = self.collectionView.indexPathsForVisibleItems for ind in visibleIndexes { - if let obj = objectAtIndex(ind.row) { + if let obj = objectAtIndex((ind as NSIndexPath).row) { if obj.senderId == Actor.myUid() && obj.sortDate >= start && obj.sortDate <= end { - let cell = self.collectionView.cellForItemAtIndexPath(ind) + let cell = self.collectionView.cellForItem(at: ind) self.bindCell(self.collectionView, cellForRowAtIndexPath: ind, cell: cell!) } } } } - public func willUpdate() { + open func willUpdate() { isLoadedAfter = false if getCount() > 0 && !isLoaded { isLoaded = true @@ -435,16 +435,16 @@ public class AAConversationContentController: SLKTextViewController, ARDisplayLi } } - public func didUpdate() { + open func didUpdate() { if isLoadedAfter { if unreadIndex != nil { - self.collectionView.scrollToItemAtIndexPath(NSIndexPath(forItem: unreadIndex!, inSection: 0), atScrollPosition: UICollectionViewScrollPosition.Bottom, animated: false) + self.collectionView.scrollToItem(at: IndexPath(item: unreadIndex!, section: 0), at: UICollectionViewScrollPosition.bottom, animated: false) unreadIndex = nil } } } - public func onBubbleAvatarTap(view: UIView, uid: jint) { + open func onBubbleAvatarTap(_ view: UIView, uid: jint) { var controller: AAViewController! = ActorSDK.sharedActor().delegate.actorControllerForUser(Int(uid)) if controller == nil { controller = AAUserViewController(uid: Int(uid)) @@ -454,16 +454,16 @@ public class AAConversationContentController: SLKTextViewController, ARDisplayLi navigation.viewControllers = [controller] let popover = UIPopoverController(contentViewController: navigation) controller.popover = popover - popover.presentPopoverFromRect(view.bounds, inView: view, permittedArrowDirections: UIPopoverArrowDirection.Any, animated: true) + popover.present(from: view.bounds, in: view, permittedArrowDirections: UIPopoverArrowDirection.any, animated: true) } else { navigateNext(controller, removeCurrent: false) } } - public override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator) + open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) - dispatch_async(dispatch_get_main_queue(), { () -> Void in + DispatchQueue.main.async(execute: { () -> Void in // self.collectionView.collectionViewLayout.invalidateLayout() self.collectionView.performBatchUpdates(nil, completion: nil) }) @@ -474,7 +474,7 @@ public class AAConversationContentController: SLKTextViewController, ARDisplayLi /////////////////////// - func playVoiceFromPath(path:String,fileId:jlong,position:Float) { + func playVoiceFromPath(_ path:String,fileId:jlong,position:Float) { if (self.currentAudioFileId != fileId) { @@ -513,15 +513,15 @@ public class AAConversationContentController: SLKTextViewController, ARDisplayLi } - func playVideoFromPath(path:String) { + func playVideoFromPath(_ path:String) { - let player = AVPlayer(URL: NSURL(fileURLWithPath: path)) + let player = AVPlayer(url: URL(fileURLWithPath: path)) let playerController = AVPlayerViewController() playerController.player = player - self.presentViewController(playerController, animated: true) { + self.present(playerController, animated: true) { player.play() } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/AANavigationBadge.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/AANavigationBadge.swift index 751b164d47..f748dbadd3 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/AANavigationBadge.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/AANavigationBadge.swift @@ -6,38 +6,38 @@ import Foundation class AANavigationBadge { - private static var binder = AABinder() - private static let badgeView = UIImageView() - private static var badgeCount = 0 - private static var isBadgeVisible = false + fileprivate static var binder = AABinder() + fileprivate static let badgeView = UIImageView() + fileprivate static var badgeCount = 0 + fileprivate static var isBadgeVisible = false - private static var isInited = false + fileprivate static var isInited = false - class private func start() { + class fileprivate func start() { if isInited { return } isInited = true - badgeView.image = Imaging.roundedImage(UIColor(rgb: 0xfe0000), size: CGSizeMake(16, 16), radius: 8) + badgeView.image = Imaging.roundedImage(UIColor(rgb: 0xfe0000), size: CGSize(width: 16, height: 16), radius: 8) // badgeView.frame = CGRectMake(16, 22, 32, 16) badgeView.alpha = 0 let badgeText = UILabel() badgeText.text = "0" - badgeText.textColor = UIColor.whiteColor() + badgeText.textColor = UIColor.white // badgeText.frame = CGRectMake(0, 0, 32, 16) - badgeText.font = UIFont.systemFontOfSize(12) - badgeText.textAlignment = NSTextAlignment.Center + badgeText.font = UIFont.systemFont(ofSize: 12) + badgeText.textAlignment = NSTextAlignment.center badgeView.addSubview(badgeText) - UIApplication.sharedApplication().windows.first!.addSubview(badgeView) + UIApplication.shared.windows.first!.addSubview(badgeView) // Bind badge counter binder.bind(Actor.getGlobalState().globalCounter, closure: { (value: JavaLangInteger?) -> () in if let v = value { - self.badgeCount = Int(v.integerValue) + self.badgeCount = Int(v.intValue) } else { self.badgeCount = 0 } @@ -49,13 +49,13 @@ class AANavigationBadge { self.badgeView.hideView() } - badgeText.frame = CGRectMake(0, 0, 128, 16) + badgeText.frame = CGRect(x: 0, y: 0, width: 128, height: 16) badgeText.sizeToFit() if badgeText.frame.width < 8 { - self.badgeView.frame = CGRectMake(16, 22, 16, 16) + self.badgeView.frame = CGRect(x: 16, y: 22, width: 16, height: 16) } else { - self.badgeView.frame = CGRectMake(16, 22, badgeText.frame.width + 8, 16) + self.badgeView.frame = CGRect(x: 16, y: 22, width: badgeText.frame.width + 8, height: 16) } badgeText.frame = self.badgeView.bounds }) @@ -80,4 +80,4 @@ class AANavigationBadge { isBadgeVisible = false self.badgeView.hideView() } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleBaseFileCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleBaseFileCell.swift index e8edb7ed1b..1370ba3d2e 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleBaseFileCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleBaseFileCell.swift @@ -4,17 +4,17 @@ import Foundation -public class AABubbleBaseFileCell: AABubbleCell { +open class AABubbleBaseFileCell: AABubbleCell { - private var bindGeneration = 0; + fileprivate var bindGeneration = 0; - private var bindedDownloadFile: jlong? = nil - private var bindedDownloadCallback: AAFileCallback? = nil + fileprivate var bindedDownloadFile: jlong? = nil + fileprivate var bindedDownloadCallback: AAFileCallback? = nil - private var bindedUploadFile: jlong? = nil - private var bindedUploadCallback: AAUploadFileCallback? = nil + fileprivate var bindedUploadFile: jlong? = nil + fileprivate var bindedUploadCallback: AAUploadFileCallback? = nil - public func fileBind(message: ACMessage, autoDownload: Bool) { + open func fileBind(_ message: ACMessage, autoDownload: Bool) { if let doc = message.content as? ACDocumentContent { let selfGeneration = prepareBind() @@ -22,7 +22,7 @@ public class AABubbleBaseFileCell: AABubbleCell { if let source = doc.getSource() as? ACFileRemoteSource { let fileReference = source.getFileReference(); - bindedDownloadFile = fileReference.getFileId() + bindedDownloadFile = fileReference?.getFileId() bindedDownloadCallback = AAFileCallback(notDownloaded: { () -> () in if (self.bindGeneration != selfGeneration) { return @@ -40,7 +40,7 @@ public class AABubbleBaseFileCell: AABubbleCell { self.fileStateChanged(reference, progress: nil, isPaused: false, isUploading: false, selfGeneration: selfGeneration) }) - Actor.bindRawFileWithReference(fileReference, autoStart: autoDownload, withCallback: bindedDownloadCallback) + Actor.bindRawFile(with: fileReference, autoStart: autoDownload, with: bindedDownloadCallback) } else if let source = doc.getSource() as? ACFileLocalSource { let fileReference = source.getFileDescriptor(); @@ -62,7 +62,7 @@ public class AABubbleBaseFileCell: AABubbleCell { self.fileStateChanged(fileReference, progress: nil, isPaused: false, isUploading: false, selfGeneration: selfGeneration) }); - Actor.bindRawUploadFileWithRid(message.rid, withCallback: bindedUploadCallback) + Actor.bindRawUploadFile(withRid: message.rid, with: bindedUploadCallback) } else { fatalError("Unsupported file source") } @@ -72,7 +72,7 @@ public class AABubbleBaseFileCell: AABubbleCell { let selfGeneration = prepareBind() - bindedDownloadFile = file.reference.getFileId() + bindedDownloadFile = file?.reference.getFileId() bindedDownloadCallback = AAFileCallback(notDownloaded: { () -> () in if (self.bindGeneration != selfGeneration) { return @@ -90,13 +90,13 @@ public class AABubbleBaseFileCell: AABubbleCell { self.fileStateChanged(reference, progress: nil, isPaused: false, isUploading: false, selfGeneration: selfGeneration) }) - Actor.bindRawFileWithReference(ACFileReference(ARApiFileLocation: file.reference.getFileLocation(), withNSString: file.reference.fileName, withInt: file.reference.fileSize), autoStart: autoDownload, withCallback: bindedDownloadCallback) + Actor.bindRawFile(with: ACFileReference(arApiFileLocation: file?.reference.getFileLocation(), with: file?.reference.fileName, with: (file?.reference.fileSize)!), autoStart: autoDownload, with: bindedDownloadCallback) } else { fatalError("Unsupported message type") } } - public func bindFile(fileReference: ACFileReference, autoDownload: Bool) { + open func bindFile(_ fileReference: ACFileReference, autoDownload: Bool) { let selfGeneration = prepareBind() @@ -118,10 +118,10 @@ public class AABubbleBaseFileCell: AABubbleCell { self.fileStateChanged(reference, progress: nil, isPaused: false, isUploading: false, selfGeneration: selfGeneration) }) - Actor.bindRawFileWithReference(fileReference, autoStart: autoDownload, withCallback: bindedDownloadCallback) + Actor.bindRawFile(with: fileReference, autoStart: autoDownload, with: bindedDownloadCallback) } - private func prepareBind() -> Int { + fileprivate func prepareBind() -> Int { // Next generation of binding bindGeneration += 1 @@ -134,11 +134,11 @@ public class AABubbleBaseFileCell: AABubbleCell { return selfGeneration } - public func fileStateChanged(reference: String?, progress: Int?, isPaused: Bool, isUploading: Bool, selfGeneration: Int) { + open func fileStateChanged(_ reference: String?, progress: Int?, isPaused: Bool, isUploading: Bool, selfGeneration: Int) { } - public func runOnUiThread(selfGeneration: Int, closure: (()->())?) -> Bool { + open func runOnUiThread(_ selfGeneration: Int, closure: (()->())?) -> Bool { if (selfGeneration != self.bindGeneration) { return false } @@ -157,17 +157,17 @@ public class AABubbleBaseFileCell: AABubbleCell { return res } - public func fileUnbind() { + open func fileUnbind() { if (bindedDownloadFile != nil && bindedDownloadCallback != nil) { - Actor.unbindRawFileWithFileId(bindedDownloadFile!, autoCancel: false, withCallback: bindedDownloadCallback) + Actor.unbindRawFile(withFileId: bindedDownloadFile!, autoCancel: false, with: bindedDownloadCallback) bindedDownloadFile = nil bindedDownloadCallback = nil } if (bindedUploadFile != nil && bindedUploadCallback != nil) { - Actor.unbindRawUploadFileWithRid(bindedUploadFile!, withCallback: bindedUploadCallback) + Actor.unbindRawUploadFile(withRid: bindedUploadFile!, with: bindedUploadCallback) bindedUploadFile = nil bindedUploadCallback = nil } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleCell.swift index 650c98c357..277ff596d3 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleCell.swift @@ -10,17 +10,17 @@ import UIKit */ public enum BubbleType { // Outcome text bubble - case TextOut + case textOut // Income text bubble - case TextIn + case textIn // Outcome media bubble - case MediaOut + case mediaOut // Income media bubble - case MediaIn + case mediaIn // Service bubble - case Service + case service // Sticker bubble - case Sticker + case sticker } /** @@ -28,9 +28,9 @@ public enum BubbleType { */ public protocol AABubbleLayouter { - func isSuitable(message: ACMessage) -> Bool + func isSuitable(_ message: ACMessage) -> Bool - func buildLayout(peer: ACPeer, message: ACMessage) -> AACellLayout + func buildLayout(_ peer: ACPeer, message: ACMessage) -> AACellLayout func cellClass() -> AnyClass } @@ -44,52 +44,52 @@ extension AABubbleLayouter { /** Root class for bubble cells */ -public class AABubbleCell: UICollectionViewCell { - - public static let bubbleContentTop: CGFloat = 6 - public static let bubbleContentBottom: CGFloat = 6 - public static let bubbleTop: CGFloat = 3 - public static let bubbleTopCompact: CGFloat = 1 - public static let bubbleBottom: CGFloat = 3 - public static let bubbleBottomCompact: CGFloat = 1 - public static let avatarPadding: CGFloat = 39 - public static let dateSize: CGFloat = 30 - public static let newMessageSize: CGFloat = 30 +open class AABubbleCell: UICollectionViewCell { + + open static let bubbleContentTop: CGFloat = 6 + open static let bubbleContentBottom: CGFloat = 6 + open static let bubbleTop: CGFloat = 3 + open static let bubbleTopCompact: CGFloat = 1 + open static let bubbleBottom: CGFloat = 3 + open static let bubbleBottomCompact: CGFloat = 1 + open static let avatarPadding: CGFloat = 39 + open static let dateSize: CGFloat = 30 + open static let newMessageSize: CGFloat = 30 // // Cached text bubble images // - private static var cachedOutTextBg = UIImage.tinted("BubbleOutgoingFull", color: ActorSDK.sharedActor().style.chatTextBubbleOutColor) + fileprivate static var cachedOutTextBg = UIImage.tinted("BubbleOutgoingFull", color: ActorSDK.sharedActor().style.chatTextBubbleOutColor) - private static var cachedOutTextBgShadow = ActorSDK.sharedActor().style.bubbleShadowEnabled ? UIImage.tinted("BubbleOutgoingFull", color: ActorSDK.sharedActor().style.chatTextBubbleShadowColor) : UIImage() + fileprivate static var cachedOutTextBgShadow = ActorSDK.sharedActor().style.bubbleShadowEnabled ? UIImage.tinted("BubbleOutgoingFull", color: ActorSDK.sharedActor().style.chatTextBubbleShadowColor) : UIImage() - private static var cachedOutTextBgBorder = UIImage.tinted("BubbleOutgoingFullBorder", color: ActorSDK.sharedActor().style.chatTextBubbleOutBorderColor) - private static var cachedOutTextCompactBg = UIImage.tinted("BubbleOutgoingPartial", color: ActorSDK.sharedActor().style.chatTextBubbleOutColor) - private static var cachedOutTextCompactBgShadow = ActorSDK.sharedActor().style.bubbleShadowEnabled ? UIImage.tinted("BubbleOutgoingPartial", color: ActorSDK.sharedActor().style.chatTextBubbleShadowColor) : UIImage() - private static var cachedOutTextCompactSelectedBg = UIImage.tinted("BubbleOutgoingPartial", color: ActorSDK.sharedActor().style.chatTextBubbleOutSelectedColor) - private static var cachedOutTextCompactBgBorder = UIImage.tinted("BubbleOutgoingPartialBorder", color: ActorSDK.sharedActor().style.chatTextBubbleOutBorderColor) + fileprivate static var cachedOutTextBgBorder = UIImage.tinted("BubbleOutgoingFullBorder", color: ActorSDK.sharedActor().style.chatTextBubbleOutBorderColor) + fileprivate static var cachedOutTextCompactBg = UIImage.tinted("BubbleOutgoingPartial", color: ActorSDK.sharedActor().style.chatTextBubbleOutColor) + fileprivate static var cachedOutTextCompactBgShadow = ActorSDK.sharedActor().style.bubbleShadowEnabled ? UIImage.tinted("BubbleOutgoingPartial", color: ActorSDK.sharedActor().style.chatTextBubbleShadowColor) : UIImage() + fileprivate static var cachedOutTextCompactSelectedBg = UIImage.tinted("BubbleOutgoingPartial", color: ActorSDK.sharedActor().style.chatTextBubbleOutSelectedColor) + fileprivate static var cachedOutTextCompactBgBorder = UIImage.tinted("BubbleOutgoingPartialBorder", color: ActorSDK.sharedActor().style.chatTextBubbleOutBorderColor) - private static var cachedInTextBg = UIImage.tinted("BubbleIncomingFull", color: ActorSDK.sharedActor().style.chatTextBubbleInColor) - private static var cachedInTextBgShadow = ActorSDK.sharedActor().style.bubbleShadowEnabled ? UIImage.tinted("BubbleIncomingFull", color: ActorSDK.sharedActor().style.chatTextBubbleShadowColor) : UIImage() - private static var cachedInTextBgBorder = UIImage.tinted("BubbleIncomingFullBorder", color: ActorSDK.sharedActor().style.chatTextBubbleInBorderColor) - private static var cachedInTextCompactBg = UIImage.tinted("BubbleIncomingPartial", color: ActorSDK.sharedActor().style.chatTextBubbleInColor) - private static var cachedInTextCompactBgShadow = ActorSDK.sharedActor().style.bubbleShadowEnabled ?UIImage.tinted("BubbleIncomingPartial", color: ActorSDK.sharedActor().style.chatTextBubbleShadowColor) : UIImage() - private static var cachedInTextCompactSelectedBg = UIImage.tinted("BubbleIncomingPartial", color: ActorSDK.sharedActor().style.chatTextBubbleInSelectedColor) - private static var cachedInTextCompactBgBorder = UIImage.tinted("BubbleIncomingPartialBorder", color: ActorSDK.sharedActor().style.chatTextBubbleInBorderColor) + fileprivate static var cachedInTextBg = UIImage.tinted("BubbleIncomingFull", color: ActorSDK.sharedActor().style.chatTextBubbleInColor) + fileprivate static var cachedInTextBgShadow = ActorSDK.sharedActor().style.bubbleShadowEnabled ? UIImage.tinted("BubbleIncomingFull", color: ActorSDK.sharedActor().style.chatTextBubbleShadowColor) : UIImage() + fileprivate static var cachedInTextBgBorder = UIImage.tinted("BubbleIncomingFullBorder", color: ActorSDK.sharedActor().style.chatTextBubbleInBorderColor) + fileprivate static var cachedInTextCompactBg = UIImage.tinted("BubbleIncomingPartial", color: ActorSDK.sharedActor().style.chatTextBubbleInColor) + fileprivate static var cachedInTextCompactBgShadow = ActorSDK.sharedActor().style.bubbleShadowEnabled ?UIImage.tinted("BubbleIncomingPartial", color: ActorSDK.sharedActor().style.chatTextBubbleShadowColor) : UIImage() + fileprivate static var cachedInTextCompactSelectedBg = UIImage.tinted("BubbleIncomingPartial", color: ActorSDK.sharedActor().style.chatTextBubbleInSelectedColor) + fileprivate static var cachedInTextCompactBgBorder = UIImage.tinted("BubbleIncomingPartialBorder", color: ActorSDK.sharedActor().style.chatTextBubbleInBorderColor) // // Cached media bubble images // - private static let cachedMediaBg = UIImage.tinted("BubbleOutgoingPartial", color: ActorSDK.sharedActor().style.chatMediaBubbleColor) - private static var cachedMediaBgBorder = UIImage.tinted("BubbleOutgoingPartialBorder", color: ActorSDK.sharedActor().style.chatMediaBubbleBorderColor) + fileprivate static let cachedMediaBg = UIImage.tinted("BubbleOutgoingPartial", color: ActorSDK.sharedActor().style.chatMediaBubbleColor) + fileprivate static var cachedMediaBgBorder = UIImage.tinted("BubbleOutgoingPartialBorder", color: ActorSDK.sharedActor().style.chatMediaBubbleBorderColor) // // Cached Service bubble images // - private static var cachedServiceBg:UIImage = Imaging.roundedImage(ActorSDK.sharedActor().style.chatServiceBubbleColor, size: CGSizeMake(18, 18), radius: 9) + fileprivate static var cachedServiceBg:UIImage = Imaging.roundedImage(ActorSDK.sharedActor().style.chatServiceBubbleColor, size: CGSize(width: 18, height: 18), radius: 9) // // Cached Date bubble images @@ -97,28 +97,28 @@ public class AABubbleCell: UICollectionViewCell { // private static var dateBgImage = Imaging.roundedImage(ActorSDK.sharedActor().style.chatDateBubbleColor, size: CGSizeMake(18, 18), radius: 9) - private static var dateBgImage = ActorSDK.sharedActor().style.statusBackgroundImage + fileprivate static var dateBgImage = ActorSDK.sharedActor().style.statusBackgroundImage // MARK: - // MARK: Public vars // Views - public let avatarView = AAAvatarView() - public var avatarAdded: Bool = false + open let avatarView = AAAvatarView() + open var avatarAdded: Bool = false - public let bubble = UIImageView() - public let bubbleShadow = UIImageView() - public let bubbleBorder = UIImageView() + open let bubble = UIImageView() + open let bubbleShadow = UIImageView() + open let bubbleBorder = UIImageView() - private let dateText = UILabel() - private let dateBg = UIImageView() + fileprivate let dateText = UILabel() + fileprivate let dateBg = UIImageView() - private let newMessage = UILabel() + fileprivate let newMessage = UILabel() // Layout - public var contentInsets : UIEdgeInsets = UIEdgeInsets() - public var bubbleInsets : UIEdgeInsets = UIEdgeInsets() - public var fullContentInsets : UIEdgeInsets { + open var contentInsets : UIEdgeInsets = UIEdgeInsets() + open var bubbleInsets : UIEdgeInsets = UIEdgeInsets() + open var fullContentInsets : UIEdgeInsets { get { return UIEdgeInsets( top: contentInsets.top + bubbleInsets.top + (isShowDate ? AABubbleCell.dateSize : 0) + (isShowNewMessages ? AABubbleCell.newMessageSize : 0), @@ -127,26 +127,26 @@ public class AABubbleCell: UICollectionViewCell { right: contentInsets.right + bubbleInsets.right) } } - public var needLayout: Bool = true + open var needLayout: Bool = true - public let groupContentInsetY = 20.0 - public let groupContentInsetX = 40.0 - public var bubbleVerticalSpacing: CGFloat = 6.0 - public let bubblePadding: CGFloat = 6; - public let bubbleMediaPadding: CGFloat = 10; + open let groupContentInsetY = 20.0 + open let groupContentInsetX = 40.0 + open var bubbleVerticalSpacing: CGFloat = 6.0 + open let bubblePadding: CGFloat = 6; + open let bubbleMediaPadding: CGFloat = 10; // Binded data - public var peer: ACPeer! - public weak var controller: AAConversationContentController! - public var isGroup: Bool = false - public var isFullSize: Bool! - public var bindedSetting: AACellSetting? - - public var bindedMessage: ACMessage? = nil - public var bubbleType:BubbleType? = nil - public var isOut: Bool = false - public var isShowDate: Bool = false - public var isShowNewMessages: Bool = false + open var peer: ACPeer! + open weak var controller: AAConversationContentController! + open var isGroup: Bool = false + open var isFullSize: Bool! + open var bindedSetting: AACellSetting? + + open var bindedMessage: ACMessage? = nil + open var bubbleType:BubbleType? = nil + open var isOut: Bool = false + open var isShowDate: Bool = false + open var isShowNewMessages: Bool = false var appStyle: ActorStyle { get { @@ -165,22 +165,23 @@ public class AABubbleCell: UICollectionViewCell { dateBg.image = AABubbleCell.dateBgImage dateText.font = UIFont.mediumSystemFontOfSize(12) dateText.textColor = appStyle.chatDateTextColor - dateText.contentMode = UIViewContentMode.Center - dateText.textAlignment = NSTextAlignment.Center + dateText.contentMode = UIViewContentMode.center + dateText.textAlignment = NSTextAlignment.center newMessage.font = UIFont.mediumSystemFontOfSize(14) newMessage.textColor = appStyle.chatUnreadTextColor - newMessage.contentMode = UIViewContentMode.Center - newMessage.textAlignment = NSTextAlignment.Center + newMessage.contentMode = UIViewContentMode.center + newMessage.textAlignment = NSTextAlignment.center newMessage.backgroundColor = appStyle.chatUnreadBgColor newMessage.text = AALocalized("ChatNewMessages") //"New Messages" - contentView.transform = CGAffineTransformMake(1, 0, 0, -1, 0, 0) + contentView.transform = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) - ActorSDK.sharedActor().style.bubbleShadowEnabled ? contentView.addSubview(bubbleShadow) : print("go to light!") - + if appStyle.bubbleShadowEnabled { + contentView.addSubview(bubbleShadow) + } contentView.addSubview(bubble) contentView.addSubview(bubbleBorder) contentView.addSubview(newMessage) @@ -189,9 +190,9 @@ public class AABubbleCell: UICollectionViewCell { avatarView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(AABubbleCell.avatarDidTap))) - avatarView.userInteractionEnabled = true + avatarView.isUserInteractionEnabled = true - backgroundColor = UIColor.clearColor() + backgroundColor = UIColor.clear // Speed up animations self.layer.speed = 1.5 @@ -206,7 +207,7 @@ public class AABubbleCell: UICollectionViewCell { fatalError("init(coder:) has not been implemented") } - func setConfig(peer: ACPeer, controller: AAConversationContentController) { + func setConfig(_ peer: ACPeer, controller: AAConversationContentController) { self.peer = peer self.controller = controller if (peer.isGroup && !isFullSize) { @@ -214,22 +215,22 @@ public class AABubbleCell: UICollectionViewCell { } } - public override func canBecomeFirstResponder() -> Bool { + open override var canBecomeFirstResponder : Bool { return false } - public override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool { - if action == #selector(NSObject.delete(_:)) { - return true - } - return false - } - - public override func delete(sender: AnyObject?) { - let rids = IOSLongArray(length: 1) - rids.replaceLongAtIndex(0, withLong: bindedMessage!.rid) - Actor.deleteMessagesWithPeer(self.peer, withRids: rids) - } +// open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { +// if action == #selector(Object.delete(_:)) { +// return true +// } +// return false +// } + +// open override func delete(_ sender: Any?) { +// let rids = IOSLongArray(length: 1) +// rids?.replaceLong(at: 0, withLong: bindedMessage!.rid) +// Actor.deleteMessages(with: self.peer, withRids: rids) +// } func avatarDidTap() { if bindedMessage != nil { @@ -237,7 +238,7 @@ public class AABubbleCell: UICollectionViewCell { } } - public func performBind(message: ACMessage, receiveDate: jlong, readDate: jlong, setting: AACellSetting, isShowNewMessages: Bool, layout: AACellLayout) { + open func performBind(_ message: ACMessage, receiveDate: jlong, readDate: jlong, setting: AACellSetting, isShowNewMessages: Bool, layout: AACellLayout) { var reuse = false if (bindedMessage != nil && bindedMessage?.rid == message.rid) { @@ -256,11 +257,11 @@ public class AABubbleCell: UICollectionViewCell { let group = Actor.getGroupWithGid(self.peer.peerId) let avatar: ACAvatar? = group.getAvatarModel().get() let name = group.getNameModel().get() - avatarView.bind(name, id: Int(user.getId()), avatar: avatar) + avatarView.bind(name!, id: Int(user.getId()), avatar: avatar) } else { let avatar: ACAvatar? = user.getAvatarModel().get() let name = user.getNameModel().get() - avatarView.bind(name, id: Int(user.getId()), avatar: avatar) + avatarView.bind(name!, id: Int(user.getId()), avatar: avatar) } if !avatarAdded { contentView.addSubview(avatarView) @@ -289,72 +290,56 @@ public class AABubbleCell: UICollectionViewCell { } } - public func bind(message: ACMessage, receiveDate: jlong, readDate: jlong, reuse: Bool, cellLayout: AACellLayout, setting: AACellSetting) { + open func bind(_ message: ACMessage, receiveDate: jlong, readDate: jlong, reuse: Bool, cellLayout: AACellLayout, setting: AACellSetting) { fatalError("bind(message:) has not been implemented") } - public func bindBubbleType(type: BubbleType, isCompact: Bool) { + open func bindBubbleType(_ type: BubbleType, isCompact: Bool) { self.bubbleType = type // Update Bubble background images switch(type) { - case BubbleType.TextIn: + case BubbleType.textIn: if (isCompact) { - bubbleShadow.image = AABubbleCell.cachedInTextCompactBgShadow - bubbleShadow.highlightedImage = AABubbleCell.cachedInTextCompactBgShadow bubble.image = AABubbleCell.cachedInTextCompactBg bubbleBorder.image = AABubbleCell.cachedInTextCompactBgBorder - bubble.highlightedImage = AABubbleCell.cachedInTextCompactSelectedBg - bubbleBorder.highlightedImage = AABubbleCell.cachedInTextCompactBgBorder + bubbleShadow.image = AABubbleCell.cachedInTextCompactBgShadow } else { - bubbleShadow.image = AABubbleCell.cachedInTextBgShadow - bubbleShadow.highlightedImage = AABubbleCell.cachedInTextBgShadow bubble.image = AABubbleCell.cachedInTextBg bubbleBorder.image = AABubbleCell.cachedInTextBgBorder - bubble.highlightedImage = AABubbleCell.cachedInTextBg - bubbleBorder.highlightedImage = AABubbleCell.cachedInTextBgBorder + bubbleShadow.image = AABubbleCell.cachedInTextBgShadow } break - case BubbleType.TextOut: + case BubbleType.textOut: if (isCompact) { - bubbleShadow.image = AABubbleCell.cachedOutTextCompactBgShadow - bubbleShadow.highlightedImage = AABubbleCell.cachedOutTextCompactBgShadow bubble.image = AABubbleCell.cachedOutTextCompactBg bubbleBorder.image = AABubbleCell.cachedOutTextCompactBgBorder - bubble.highlightedImage = AABubbleCell.cachedOutTextCompactSelectedBg - bubbleBorder.highlightedImage = AABubbleCell.cachedOutTextCompactBgBorder + bubbleShadow.image = AABubbleCell.cachedOutTextCompactBgShadow } else { - bubbleShadow.image = AABubbleCell.cachedOutTextBgShadow - bubbleShadow.highlightedImage = AABubbleCell.cachedOutTextBgShadow bubble.image = AABubbleCell.cachedOutTextBg bubbleBorder.image = AABubbleCell.cachedOutTextBgBorder - bubble.highlightedImage = AABubbleCell.cachedOutTextBg - bubbleBorder.highlightedImage = AABubbleCell.cachedOutTextBgBorder + bubbleShadow.image = AABubbleCell.cachedOutTextBgShadow } break - case BubbleType.MediaIn: + case BubbleType.mediaIn: bubble.image = AABubbleCell.cachedMediaBg bubbleBorder.image = AABubbleCell.cachedMediaBgBorder - bubble.highlightedImage = AABubbleCell.cachedMediaBg - bubbleBorder.highlightedImage = AABubbleCell.cachedMediaBgBorder + bubbleShadow.image = nil break - case BubbleType.MediaOut: + case BubbleType.mediaOut: bubble.image = AABubbleCell.cachedMediaBg bubbleBorder.image = AABubbleCell.cachedMediaBgBorder - bubble.highlightedImage = AABubbleCell.cachedMediaBg - bubbleBorder.highlightedImage = AABubbleCell.cachedMediaBgBorder + bubbleShadow.image = nil break - case BubbleType.Service: + case BubbleType.service: bubble.image = AABubbleCell.cachedServiceBg bubbleBorder.image = nil - bubble.highlightedImage = AABubbleCell.cachedServiceBg - bubbleBorder.highlightedImage = nil + bubbleShadow.image = nil break - case BubbleType.Sticker: + case BubbleType.sticker: bubble.image = nil; bubbleBorder.image = nil - bubble.highlightedImage = nil; - bubbleBorder.highlightedImage = nil + bubbleShadow.image = nil break } } @@ -362,63 +347,47 @@ public class AABubbleCell: UICollectionViewCell { func updateView() { let type = self.bubbleType! as BubbleType switch (type) { - case BubbleType.TextIn: + case BubbleType.textIn: if (!isFullSize!) { - bubbleShadow.image = AABubbleCell.cachedInTextCompactBgShadow - bubbleShadow.highlightedImage = AABubbleCell.cachedInTextCompactBgShadow bubble.image = AABubbleCell.cachedInTextCompactBg bubbleBorder.image = AABubbleCell.cachedInTextCompactBgBorder - bubble.highlightedImage = AABubbleCell.cachedInTextCompactSelectedBg - bubbleBorder.highlightedImage = AABubbleCell.cachedInTextCompactBgBorder + bubbleShadow.image = AABubbleCell.cachedInTextCompactBgShadow } else { - bubbleShadow.image = AABubbleCell.cachedInTextBgShadow - bubbleShadow.highlightedImage = AABubbleCell.cachedInTextBgShadow bubble.image = AABubbleCell.cachedInTextBg bubbleBorder.image = AABubbleCell.cachedInTextBgBorder - bubble.highlightedImage = AABubbleCell.cachedInTextBg - bubbleBorder.highlightedImage = AABubbleCell.cachedInTextBgBorder + bubbleShadow.image = AABubbleCell.cachedInTextBgShadow } break - case BubbleType.TextOut: + case BubbleType.textOut: if (!isFullSize!) { - bubbleShadow.image = AABubbleCell.cachedOutTextCompactBgShadow - bubbleShadow.highlightedImage = AABubbleCell.cachedOutTextCompactBgShadow bubble.image = AABubbleCell.cachedOutTextCompactBg bubbleBorder.image = AABubbleCell.cachedOutTextCompactBgBorder - bubble.highlightedImage = AABubbleCell.cachedOutTextCompactSelectedBg - bubbleBorder.highlightedImage = AABubbleCell.cachedOutTextCompactBgBorder + bubbleShadow.image = AABubbleCell.cachedOutTextCompactBgShadow } else { - bubbleShadow.image = AABubbleCell.cachedOutTextBgShadow - bubbleShadow.highlightedImage = AABubbleCell.cachedOutTextBgShadow bubble.image = AABubbleCell.cachedOutTextBg bubbleBorder.image = AABubbleCell.cachedOutTextBgBorder - bubble.highlightedImage = AABubbleCell.cachedOutTextBg - bubbleBorder.highlightedImage = AABubbleCell.cachedOutTextBgBorder + bubbleShadow.image = AABubbleCell.cachedOutTextBgShadow } break - case BubbleType.MediaIn: + case BubbleType.mediaIn: bubble.image = AABubbleCell.cachedMediaBg bubbleBorder.image = AABubbleCell.cachedMediaBgBorder - bubble.highlightedImage = AABubbleCell.cachedMediaBg - bubbleBorder.highlightedImage = AABubbleCell.cachedMediaBgBorder + bubbleShadow.image = nil break - case BubbleType.MediaOut: + case BubbleType.mediaOut: bubble.image = AABubbleCell.cachedMediaBg bubbleBorder.image = AABubbleCell.cachedMediaBgBorder - bubble.highlightedImage = AABubbleCell.cachedMediaBg - bubbleBorder.highlightedImage = AABubbleCell.cachedMediaBgBorder + bubbleShadow.image = nil break - case BubbleType.Service: + case BubbleType.service: bubble.image = AABubbleCell.cachedServiceBg bubbleBorder.image = nil - bubble.highlightedImage = AABubbleCell.cachedServiceBg - bubbleBorder.highlightedImage = nil + bubbleShadow.image = nil break - case BubbleType.Sticker: + case BubbleType.sticker: bubble.image = nil; bubbleBorder.image = nil - bubble.highlightedImage = nil; - bubbleBorder.highlightedImage = nil + bubbleShadow.image = nil break } } @@ -426,7 +395,7 @@ public class AABubbleCell: UICollectionViewCell { // MARK: - // MARK: Layout - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() UIView.performWithoutAnimation { () -> Void in @@ -443,17 +412,17 @@ public class AABubbleCell: UICollectionViewCell { func layoutAnchor() { if (isShowDate) { - dateText.frame = CGRectMake(0, 0, 1000, 1000) + dateText.frame = CGRect(x: 0, y: 0, width: 1000, height: 1000) dateText.sizeToFit() - dateText.frame = CGRectMake( - (self.contentView.frame.size.width-dateText.frame.width)/2, 8, dateText.frame.width, 18) - dateBg.frame = CGRectMake(dateText.frame.minX - 8, dateText.frame.minY, dateText.frame.width + 16, 18) + dateText.frame = CGRect( + x: (self.contentView.frame.size.width-dateText.frame.width)/2, y: 8, width: dateText.frame.width, height: 18) + dateBg.frame = CGRect(x: dateText.frame.minX - 8, y: dateText.frame.minY, width: dateText.frame.width + 16, height: 18) - dateText.hidden = false - dateBg.hidden = false + dateText.isHidden = false + dateBg.isHidden = false } else { - dateText.hidden = true - dateBg.hidden = true + dateText.isHidden = true + dateBg.isHidden = true } if (isShowNewMessages) { @@ -461,14 +430,14 @@ public class AABubbleCell: UICollectionViewCell { if (isShowDate) { top += AABubbleCell.dateSize } - newMessage.hidden = false - newMessage.frame = CGRectMake(0, top + CGFloat(2), self.contentView.frame.width, AABubbleCell.newMessageSize - CGFloat(4)) + newMessage.isHidden = false + newMessage.frame = CGRect(x: 0, y: top + CGFloat(2), width: self.contentView.frame.width, height: AABubbleCell.newMessageSize - CGFloat(4)) } else { - newMessage.hidden = true + newMessage.isHidden = true } } - public func layoutContent(maxWidth: CGFloat, offsetX: CGFloat) { + open func layoutContent(_ maxWidth: CGFloat, offsetX: CGFloat) { } @@ -479,7 +448,7 @@ public class AABubbleCell: UICollectionViewCell { // Need to be called in child cells - public func layoutBubble(contentWidth: CGFloat, contentHeight: CGFloat) { + open func layoutBubble(_ contentWidth: CGFloat, contentHeight: CGFloat) { let fullWidth = contentView.bounds.width let bubbleW = contentWidth + contentInsets.left + contentInsets.right let bubbleH = contentHeight + contentInsets.top + contentInsets.bottom @@ -522,13 +491,13 @@ public class AABubbleCell: UICollectionViewCell { height: bubbleH) } - public func layoutBubble(frame: CGRect) { + open func layoutBubble(_ frame: CGRect) { bubble.frame = frame bubbleBorder.frame = frame bubbleShadow.frame = frame } - public override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { + open override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { return layoutAttributes } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleContactCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleContactCell.swift index 56a80e21a0..9dae467cd3 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleContactCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleContactCell.swift @@ -6,20 +6,20 @@ import Foundation import AddressBookUI import MessageUI -public class AABubbleContactCell: AABubbleCell, ABNewPersonViewControllerDelegate, MFMailComposeViewControllerDelegate, UINavigationControllerDelegate { +open class AABubbleContactCell: AABubbleCell, ABNewPersonViewControllerDelegate, MFMailComposeViewControllerDelegate, UINavigationControllerDelegate { - private let avatar = AAAvatarView() - private let name = UILabel() - private let contact = UILabel() - private var bindedRecords = [String]() - private let tapView = UIView() + fileprivate let avatar = AAAvatarView() + fileprivate let name = UILabel() + fileprivate let contact = UILabel() + fileprivate var bindedRecords = [String]() + fileprivate let tapView = UIView() public init(frame: CGRect) { super.init(frame: frame, isFullSize: false) name.font = UIFont.mediumSystemFontOfSize(17) - contact.font = UIFont.systemFontOfSize(15) - tapView.backgroundColor = UIColor.clearColor() + contact.font = UIFont.systemFont(ofSize: 15) + tapView.backgroundColor = UIColor.clear contentView.addSubview(avatar) contentView.addSubview(name) @@ -29,7 +29,7 @@ public class AABubbleContactCell: AABubbleCell, ABNewPersonViewControllerDelegat contentInsets = UIEdgeInsets(top: 1, left: 1, bottom: 1, right: 1) tapView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(AABubbleContactCell.contactDidTap))) - tapView.userInteractionEnabled = true + tapView.isUserInteractionEnabled = true } public required init(coder aDecoder: NSCoder) { @@ -41,11 +41,11 @@ public class AABubbleContactCell: AABubbleCell, ABNewPersonViewControllerDelegat if let c = m.content as? ACContactContent { let menuBuilder = AAMenuBuilder() let phones = c.getPhones() - for i in 0.. () in - if let url = NSURL(string: "tel:\(p)") { - if !UIApplication.sharedApplication().openURL(url) { + if let url = URL(string: "tel:\(p)") { + if !UIApplication.shared.openURL(url) { self.controller.alertUser("ErrorUnableToCall") } } else { @@ -54,48 +54,48 @@ public class AABubbleContactCell: AABubbleCell, ABNewPersonViewControllerDelegat }) } let emails = c.getEmails() - for i in 0.. () in let emailController = MFMailComposeViewController() emailController.delegate = self emailController.setToRecipients([e]) - self.controller.presentViewController(emailController, animated: true, completion: nil) + self.controller.present(emailController, animated: true, completion: nil) }) } menuBuilder.add(AALocalized("ProfileAddToContacts"), closure: { () -> () in let add = ABNewPersonViewController() add.newPersonViewDelegate = self - let person: ABRecordRef = ABPersonCreate().takeRetainedValue() + let person: ABRecord = ABPersonCreate().takeRetainedValue() let name = c.getName().trim() - let nameParts = name.componentsSeparatedByString(" ") - ABRecordSetValue(person, kABPersonFirstNameProperty, nameParts[0], nil) + let nameParts = name.components(separatedBy: " ") + ABRecordSetValue(person, kABPersonFirstNameProperty, nameParts[0] as CFTypeRef!, nil) if (nameParts.count >= 2) { - let lastName = name.substringFromIndex(nameParts[0].endIndex).trim() - ABRecordSetValue(person, kABPersonLastNameProperty, lastName, nil) + let lastName = name.substring(from: nameParts[0].endIndex).trim() + ABRecordSetValue(person, kABPersonLastNameProperty, lastName as CFTypeRef!, nil) } - if (phones.size() > 0) { - let phonesValues: ABMultiValueRef = ABMultiValueCreateMutable(UInt32(kABMultiStringPropertyType)).takeRetainedValue() - for i in 0.. 0) { + let phonesValues: ABMultiValue = ABMultiValueCreateMutable(UInt32(kABMultiStringPropertyType)).takeRetainedValue() + for i in 0.. 0) { - let phonesValues: ABMultiValueRef = ABMultiValueCreateMutable(UInt32(kABMultiStringPropertyType)).takeRetainedValue() - for i in 0.. 0) { + let phonesValues: ABMultiValue = ABMultiValueCreateMutable(UInt32(kABMultiStringPropertyType)).takeRetainedValue() + for i in 0.. Bool { - if (!ActorSDK.sharedActor().enableExperimentalFeatures) { - return false - } - +open class AABubbleContactCellLayouter: AABubbleLayouter { + open func isSuitable(_ message: ACMessage) -> Bool { if (message.content is ACContactContent) { return true } @@ -210,19 +206,19 @@ public class AABubbleContactCellLayouter: AABubbleLayouter { return false } - public func cellClass() -> AnyClass { + open func cellClass() -> AnyClass { return AABubbleContactCell.self } - public func buildLayout(peer: ACPeer, message: ACMessage) -> AACellLayout { + open func buildLayout(_ peer: ACPeer, message: ACMessage) -> AACellLayout { let content = message.content as! ACContactContent var records = [String]() for i in 0.. Void in - self.progress.hidden = true - self.fileIcon.hidden = true + UIView.animate(withDuration: 0, animations: { () -> Void in + self.progress.isHidden = true + self.fileIcon.isHidden = true }) // Bind file @@ -138,44 +138,44 @@ public class AABubbleDocumentCell: AABubbleBaseFileCell, UIDocumentInteractionCo } } - public func documentDidTap() { + open func documentDidTap() { let content = bindedMessage!.content as! ACDocumentContent if let fileSource = content.getSource() as? ACFileRemoteSource { - Actor.requestStateWithFileId(fileSource.getFileReference().getFileId(), withCallback: AAFileCallback( + Actor.requestState(withFileId: fileSource.getFileReference().getFileId(), with: AAFileCallback( notDownloaded: { () -> () in - Actor.startDownloadingWithReference(fileSource.getFileReference()) + Actor.startDownloading(with: fileSource.getFileReference()) }, onDownloading: { (progress) -> () in - Actor.cancelDownloadingWithFileId(fileSource.getFileReference().getFileId()) + Actor.cancelDownloading(withFileId: fileSource.getFileReference().getFileId()) }, onDownloaded: { (reference) -> () in - let docController = UIDocumentInteractionController(URL: NSURL(fileURLWithPath: CocoaFiles.pathFromDescriptor(reference))) + let docController = UIDocumentInteractionController(url: URL(fileURLWithPath: CocoaFiles.pathFromDescriptor(reference))) docController.delegate = self - if (docController.presentPreviewAnimated(true)) { + if (docController.presentPreview(animated: true)) { return } })) } else if let fileSource = content.getSource() as? ACFileLocalSource { let rid = bindedMessage!.rid - Actor.requestUploadStateWithRid(rid, withCallback: AAUploadFileCallback( + Actor.requestUploadState(withRid: rid, with: AAUploadFileCallback( notUploaded: { () -> () in - Actor.resumeUploadWithRid(rid) + Actor.resumeUpload(withRid: rid) }, onUploading: { (progress) -> () in - Actor.pauseUploadWithRid(rid) + Actor.pauseUpload(withRid: rid) }, onUploadedClosure: { () -> () in - let docController = UIDocumentInteractionController(URL: NSURL(fileURLWithPath: CocoaFiles.pathFromDescriptor(fileSource.getFileDescriptor()))) + let docController = UIDocumentInteractionController(url: URL(fileURLWithPath: CocoaFiles.pathFromDescriptor(fileSource.getFileDescriptor()))) docController.delegate = self - if (docController.presentPreviewAnimated(true)) { + if (docController.presentPreview(animated: true)) { return } })) } } - public override func fileStateChanged(reference: String?, progress: Int?, isPaused: Bool, isUploading: Bool, selfGeneration: Int) { + open override func fileStateChanged(_ reference: String?, progress: Int?, isPaused: Bool, isUploading: Bool, selfGeneration: Int) { self.runOnUiThread(selfGeneration) { () -> () in if isUploading { if isPaused { @@ -219,7 +219,7 @@ public class AABubbleDocumentCell: AABubbleBaseFileCell, UIDocumentInteractionCo } } - public override func layoutContent(maxWidth: CGFloat, offsetX: CGFloat) { + open override func layoutContent(_ maxWidth: CGFloat, offsetX: CGFloat) { let insets = fullContentInsets let contentWidth = self.contentView.frame.width @@ -231,63 +231,63 @@ public class AABubbleDocumentCell: AABubbleBaseFileCell, UIDocumentInteractionCo let contentLeft = self.isOut ? contentWidth - 200 - insets.right - contentInsets.left : insets.left // Content - self.titleLabel.frame = CGRectMake(contentLeft + 62, 16 + top, 200 - 64, 22) - self.sizeLabel.frame = CGRectMake(contentLeft + 62, 16 + 22 + top, 200 - 64, 22) + self.titleLabel.frame = CGRect(x: contentLeft + 62, y: 16 + top, width: 200 - 64, height: 22) + self.sizeLabel.frame = CGRect(x: contentLeft + 62, y: 16 + 22 + top, width: 200 - 64, height: 22) // Progress state - let progressRect = CGRectMake(contentLeft + 8, 12 + top, 48, 48) + let progressRect = CGRect(x: contentLeft + 8, y: 12 + top, width: 48, height: 48) self.progress.frame = progressRect - self.fileIcon.frame = CGRectMake(contentLeft + 16, 20 + top, 32, 32) + self.fileIcon.frame = CGRect(x: contentLeft + 16, y: 20 + top, width: 32, height: 32) // Message state if (self.isOut) { - self.dateLabel.frame = CGRectMake(self.bubble.frame.maxX - 70 - self.bubblePadding, self.bubble.frame.maxY - 24, 46, 26) - self.statusView.frame = CGRectMake(self.bubble.frame.maxX - 24 - self.bubblePadding, self.bubble.frame.maxY - 24, 20, 26) - self.statusView.hidden = false + self.dateLabel.frame = CGRect(x: self.bubble.frame.maxX - 70 - self.bubblePadding, y: self.bubble.frame.maxY - 24, width: 46, height: 26) + self.statusView.frame = CGRect(x: self.bubble.frame.maxX - 24 - self.bubblePadding, y: self.bubble.frame.maxY - 24, width: 20, height: 26) + self.statusView.isHidden = false } else { - self.dateLabel.frame = CGRectMake(self.bubble.frame.maxX - 47 - self.bubblePadding, self.bubble.frame.maxY - 24, 46, 26) - self.statusView.hidden = true + self.dateLabel.frame = CGRect(x: self.bubble.frame.maxX - 47 - self.bubblePadding, y: self.bubble.frame.maxY - 24, width: 46, height: 26) + self.statusView.isHidden = true } } - public func documentInteractionControllerViewControllerForPreview(controller: UIDocumentInteractionController) -> UIViewController { + open func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController { return self.controller } } -public class AABubbleDocumentCellLayout: AABubbleLayouter { +open class AABubbleDocumentCellLayout: AABubbleLayouter { - public func isSuitable(message: ACMessage) -> Bool { + open func isSuitable(_ message: ACMessage) -> Bool { return message.content is ACDocumentContent } - public func buildLayout(peer: ACPeer, message: ACMessage) -> AACellLayout { + open func buildLayout(_ peer: ACPeer, message: ACMessage) -> AACellLayout { return DocumentCellLayout(message: message, layouter: self) } - public func cellClass() -> AnyClass { + open func cellClass() -> AnyClass { return AABubbleDocumentCell.self } } -public class DocumentCellLayout: AACellLayout { +open class DocumentCellLayout: AACellLayout { - public let fileName: String - public let fileExt: String - public let fileSize: String + open let fileName: String + open let fileExt: String + open let fileSize: String - public let icon: UIImage - public let fastThumb: NSData? + open let icon: UIImage + open let fastThumb: Data? - public let autoDownload: Bool + open let autoDownload: Bool public init(fileName: String, fileExt: String, fileSize: Int, fastThumb: ACFastThumb?, date: Int64, autoDownload: Bool, layouter: AABubbleLayouter) { // File metadata self.fileName = fileName - self.fileExt = fileExt.lowercaseString + self.fileExt = fileExt.lowercased() self.fileSize = Actor.getFormatter().formatFileSize(jint(fileSize)) // Auto download flag @@ -300,37 +300,37 @@ public class DocumentCellLayout: AACellLayout { var fileName = "file_unknown" if (AAFileTypes[self.fileExt] != nil) { switch(AAFileTypes[self.fileExt]!) { - case AAFileType.Music: + case AAFileType.music: fileName = "file_music" break - case AAFileType.Doc: + case AAFileType.doc: fileName = "file_doc" break - case AAFileType.Spreadsheet: + case AAFileType.spreadsheet: fileName = "file_xls" break - case AAFileType.Video: + case AAFileType.video: fileName = "file_video" break - case AAFileType.Presentation: + case AAFileType.presentation: fileName = "file_ppt" break - case AAFileType.PDF: + case AAFileType.pdf: fileName = "file_pdf" break - case AAFileType.APK: + case AAFileType.apk: fileName = "file_apk" break - case AAFileType.RAR: + case AAFileType.rar: fileName = "file_rar" break - case AAFileType.ZIP: + case AAFileType.zip: fileName = "file_zip" break - case AAFileType.CSV: + case AAFileType.csv: fileName = "file_csv" break - case AAFileType.HTML: + case AAFileType.html: fileName = "file_html" break default: @@ -350,4 +350,4 @@ public class DocumentCellLayout: AACellLayout { public convenience init(message: ACMessage, layouter: AABubbleLayouter) { self.init(document: message.content as! ACDocumentContent, date: Int64(message.date), layouter: layouter) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleLocationCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleLocationCell.swift index c097d1f92d..7c202a9f78 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleLocationCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleLocationCell.swift @@ -8,27 +8,27 @@ import MapKit private let mapWidth: CGFloat = 200 private let mapHeight: CGFloat = 160 -public class AABubbleLocationCell: AABubbleCell { +open class AABubbleLocationCell: AABubbleCell { - private let map = AAMapFastView(mapWidth: mapWidth, mapHeight: mapHeight) + fileprivate let map = AAMapFastView(mapWidth: mapWidth, mapHeight: mapHeight) - private let pin = UIImageView() - private let timeBg = UIImageView() - private let timeLabel = UILabel() - private let statusView = UIImageView() + fileprivate let pin = UIImageView() + fileprivate let timeBg = UIImageView() + fileprivate let timeLabel = UILabel() + fileprivate let statusView = UIImageView() - private var bindedLat: Double? = nil - private var bindedLon: Double? = nil + fileprivate var bindedLat: Double? = nil + fileprivate var bindedLon: Double? = nil public init(frame: CGRect) { super.init(frame: frame, isFullSize: false) timeBg.image = ActorSDK.sharedActor().style.statusBackgroundImage - timeLabel.font = UIFont.italicSystemFontOfSize(11) + timeLabel.font = UIFont.italicSystemFont(ofSize: 11) timeLabel.textColor = appStyle.chatMediaDateColor - statusView.contentMode = UIViewContentMode.Center + statusView.contentMode = UIViewContentMode.center pin.image = UIImage.bundled("LocationPin") @@ -42,7 +42,7 @@ public class AABubbleLocationCell: AABubbleCell { contentInsets = UIEdgeInsets(top: 1, left: 1, bottom: 1, right: 1) map.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(AABubbleLocationCell.mapDidTap))) - map.userInteractionEnabled = true + map.isUserInteractionEnabled = true } public required init(coder aDecoder: NSCoder) { @@ -52,10 +52,10 @@ public class AABubbleLocationCell: AABubbleCell { func mapDidTap() { let url = "http://maps.apple.com/?q=\(bindedLat!),\(bindedLon!)" // print("url: \(url)") - UIApplication.sharedApplication().openURL(NSURL(string: url)!) + UIApplication.shared.openURL(URL(string: url)!) } - public override func bind(message: ACMessage, receiveDate: jlong, readDate: jlong, reuse: Bool, cellLayout: AACellLayout, setting: AACellSetting) { + open override func bind(_ message: ACMessage, receiveDate: jlong, readDate: jlong, reuse: Bool, cellLayout: AACellLayout, setting: AACellSetting) { let layout = cellLayout as! AALocationCellLayout @@ -72,9 +72,9 @@ public class AABubbleLocationCell: AABubbleCell { // Bind bubble if (self.isOut) { - bindBubbleType(BubbleType.MediaOut, isCompact: false) + bindBubbleType(BubbleType.mediaOut, isCompact: false) } else { - bindBubbleType(BubbleType.MediaIn, isCompact: false) + bindBubbleType(BubbleType.mediaIn, isCompact: false) } } @@ -85,7 +85,7 @@ public class AABubbleLocationCell: AABubbleCell { // Update status if (isOut) { - statusView.hidden = false + statusView.isHidden = false switch(message.messageState.toNSEnum()) { case .SENT: if message.sortDate <= readDate { @@ -112,43 +112,43 @@ public class AABubbleLocationCell: AABubbleCell { break } } else { - statusView.hidden = true + statusView.isHidden = true } } - public override func layoutContent(maxWidth: CGFloat, offsetX: CGFloat) { + open override func layoutContent(_ maxWidth: CGFloat, offsetX: CGFloat) { let insets = fullContentInsets let contentWidth = self.contentView.frame.width layoutBubble(mapWidth, contentHeight: mapHeight) if isOut { - map.frame = CGRectMake(contentWidth - insets.right - mapWidth , insets.top, mapWidth, mapHeight) + map.frame = CGRect(x: contentWidth - insets.right - mapWidth , y: insets.top, width: mapWidth, height: mapHeight) } else { - map.frame = CGRectMake(insets.left, insets.top, mapWidth, mapHeight) + map.frame = CGRect(x: insets.left, y: insets.top, width: mapWidth, height: mapHeight) } - timeLabel.frame = CGRectMake(0, 0, 1000, 1000) + timeLabel.frame = CGRect(x: 0, y: 0, width: 1000, height: 1000) timeLabel.sizeToFit() let timeWidth = (isOut ? 23 : 0) + timeLabel.bounds.width let timeHeight: CGFloat = 20 - timeLabel.frame = CGRectMake(map.frame.maxX - timeWidth - 18, map.frame.maxY - timeHeight - 6, timeLabel.frame.width, timeHeight) + timeLabel.frame = CGRect(x: map.frame.maxX - timeWidth - 18, y: map.frame.maxY - timeHeight - 6, width: timeLabel.frame.width, height: timeHeight) if (isOut) { - statusView.frame = CGRectMake(timeLabel.frame.maxX, timeLabel.frame.minY, 23, timeHeight) + statusView.frame = CGRect(x: timeLabel.frame.maxX, y: timeLabel.frame.minY, width: 23, height: timeHeight) } - pin.frame = CGRectMake((map.width - pin.image!.size.width)/2, (map.height / 2 - pin.image!.size.height), - pin.image!.size.width, pin.image!.size.height) + pin.frame = CGRect(x: (map.width - pin.image!.size.width)/2, y: (map.height / 2 - pin.image!.size.height), + width: pin.image!.size.width, height: pin.image!.size.height) - timeBg.frame = CGRectMake(timeLabel.frame.minX - 4, timeLabel.frame.minY - 1, timeWidth + 8, timeHeight + 2) + timeBg.frame = CGRect(x: timeLabel.frame.minX - 4, y: timeLabel.frame.minY - 1, width: timeWidth + 8, height: timeHeight + 2) } } -public class AALocationCellLayout: AACellLayout { +open class AALocationCellLayout: AACellLayout { let latitude: Double let longitude: Double @@ -160,21 +160,21 @@ public class AALocationCellLayout: AACellLayout { } } -public class AABubbleLocationCellLayouter: AABubbleLayouter { +open class AABubbleLocationCellLayouter: AABubbleLayouter { - public func isSuitable(message: ACMessage) -> Bool { + open func isSuitable(_ message: ACMessage) -> Bool { if (message.content is ACLocationContent) { return true } return false } - public func buildLayout(peer: ACPeer, message: ACMessage) -> AACellLayout { + open func buildLayout(_ peer: ACPeer, message: ACMessage) -> AACellLayout { let content = message.content as! ACLocationContent return AALocationCellLayout(latitude: Double(content.getLatitude()), longitude: Double(content.getLongitude()), date: Int64(message.date), layouter: self) } - public func cellClass() -> AnyClass { + open func cellClass() -> AnyClass { return AABubbleLocationCell.self } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleMediaCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleMediaCell.swift index d555ffd12b..8856d0ea85 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleMediaCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleMediaCell.swift @@ -8,12 +8,12 @@ import YYImage import YYWebImage import YYCategories -public class AABubbleMediaCell : AABubbleBaseFileCell, NYTPhotosViewControllerDelegate { +open class AABubbleMediaCell : AABubbleBaseFileCell, NYTPhotosViewControllerDelegate { // Views var preview = YYAnimatedImageView() - let progress = AAProgressView(size: CGSizeMake(64, 64)) + let progress = AAProgressView(size: CGSize(width: 64, height: 64)) let timeBg = UIImageView() let timeLabel = UILabel() let statusView = UIImageView() @@ -35,14 +35,14 @@ public class AABubbleMediaCell : AABubbleBaseFileCell, NYTPhotosViewControllerDe super.init(frame: frame, isFullSize: false) preview.autoPlayAnimatedImage = true - preview.runloopMode = NSDefaultRunLoopMode + preview.runloopMode = RunLoopMode.defaultRunLoopMode.rawValue timeBg.image = Imaging.roundedImage(ActorSDK.sharedActor().style.chatMediaDateBgColor, radius: 10) - timeLabel.font = UIFont.italicSystemFontOfSize(11) + timeLabel.font = UIFont.italicSystemFont(ofSize: 11) timeLabel.textColor = appStyle.chatMediaDateColor - statusView.contentMode = UIViewContentMode.Center + statusView.contentMode = UIViewContentMode.center contentView.addSubview(preview) contentView.addSubview(progress) @@ -53,10 +53,10 @@ public class AABubbleMediaCell : AABubbleBaseFileCell, NYTPhotosViewControllerDe contentView.addSubview(playView) preview.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(AABubbleMediaCell.mediaDidTap))) - preview.userInteractionEnabled = true + preview.isUserInteractionEnabled = true contentInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) - playView.userInteractionEnabled = false + playView.isUserInteractionEnabled = false } public required init(coder aDecoder: NSCoder) { @@ -65,7 +65,7 @@ public class AABubbleMediaCell : AABubbleBaseFileCell, NYTPhotosViewControllerDe // Binding - public override func bind(message: ACMessage, receiveDate: jlong, readDate: jlong, reuse: Bool, cellLayout: AACellLayout, setting: AACellSetting) { + open override func bind(_ message: ACMessage, receiveDate: jlong, readDate: jlong, reuse: Bool, cellLayout: AACellLayout, setting: AACellSetting) { self.bindedLayout = cellLayout as! MediaCellLayout bubbleInsets = UIEdgeInsets( @@ -78,9 +78,9 @@ public class AABubbleMediaCell : AABubbleBaseFileCell, NYTPhotosViewControllerDe // Bind bubble if (self.isOut) { - bindBubbleType(BubbleType.MediaOut, isCompact: false) + bindBubbleType(BubbleType.mediaOut, isCompact: false) } else { - bindBubbleType(BubbleType.MediaIn, isCompact: false) + bindBubbleType(BubbleType.mediaIn, isCompact: false) } // Reset content state @@ -98,7 +98,7 @@ public class AABubbleMediaCell : AABubbleBaseFileCell, NYTPhotosViewControllerDe self.preview.alpha = 0 // Show/Hide play button - self.playView.hidden = true + self.playView.isHidden = true // Rounding Animations if (message.content is ACAnimationContent) { @@ -133,7 +133,7 @@ public class AABubbleMediaCell : AABubbleBaseFileCell, NYTPhotosViewControllerDe // Update status if (isOut) { - statusView.hidden = false + statusView.isHidden = false switch(message.messageState.toNSEnum()) { case .SENT: if message.sortDate <= readDate { @@ -157,13 +157,13 @@ public class AABubbleMediaCell : AABubbleBaseFileCell, NYTPhotosViewControllerDe break } } else { - statusView.hidden = true + statusView.isHidden = true } } // File state binding - public override func fileStateChanged(reference: String?, progress: Int?, isPaused: Bool, isUploading: Bool, selfGeneration: Int) { + open override func fileStateChanged(_ reference: String?, progress: Int?, isPaused: Bool, isUploading: Bool, selfGeneration: Int) { // Loading Fast Thumb // 1. Check Current generation @@ -176,7 +176,7 @@ public class AABubbleMediaCell : AABubbleBaseFileCell, NYTPhotosViewControllerDe })) { return } if (needToLoadThumb) { - let loadedThumb = YYImage(data: bindedLayout.fastThumb!)!.imageByBlurSoft()! + let loadedThumb = YYImage(data: bindedLayout.fastThumb!)!.byBlurSoft()! .roundCorners(bindedLayout.screenSize.width, h: bindedLayout.screenSize.height, roundSize: 14) if !(runOnUiThread(selfGeneration, closure: { self.thumb = loadedThumb @@ -201,9 +201,9 @@ public class AABubbleMediaCell : AABubbleBaseFileCell, NYTPhotosViewControllerDe if let r = reference { if !self.previewShown { self.previewShown = true - self.preview.yy_setImageWithURL(NSURL.fileURLWithPath(CocoaFiles.pathFromDescriptor(r)), + self.preview.yy_setImage(with: URL(fileURLWithPath: CocoaFiles.pathFromDescriptor(r)), placeholder: self.thumb, - options: YYWebImageOptions.SetImageWithFadeAnimation, + options: YYWebImageOptions.setImageWithFadeAnimation, progress: nil, transform: { (img, url) -> UIImage? in return img.roundCorners(self.bindedLayout.screenSize.width, h: self.bindedLayout.screenSize.height, roundSize: 14) @@ -221,8 +221,8 @@ public class AABubbleMediaCell : AABubbleBaseFileCell, NYTPhotosViewControllerDe if let r = reference { if !self.previewShown { self.previewShown = true - self.preview.yy_setImageWithURL(NSURL.fileURLWithPath(CocoaFiles.pathFromDescriptor(r)), - placeholder: self.thumb, options: YYWebImageOptions.SetImageWithFadeAnimation, + self.preview.yy_setImage(with: URL(fileURLWithPath: CocoaFiles.pathFromDescriptor(r)), + placeholder: self.thumb, options: YYWebImageOptions.setImageWithFadeAnimation, progress: nil, transform: nil,completion: nil) self.preview.showViewAnimated() } @@ -255,12 +255,12 @@ public class AABubbleMediaCell : AABubbleBaseFileCell, NYTPhotosViewControllerDe // Play Button if self.bindedMessage?.content is ACAnimationContent { if ActorSDK.sharedActor().isGIFAutoplayEnabled { - self.playView.hidden = true + self.playView.isHidden = true } else { - self.playView.hidden = false + self.playView.isHidden = false } } else { - self.playView.hidden = !(self.bindedMessage?.content is ACVideoContent) + self.playView.isHidden = !(self.bindedMessage?.content is ACVideoContent) } } else { if isPaused { @@ -279,62 +279,62 @@ public class AABubbleMediaCell : AABubbleBaseFileCell, NYTPhotosViewControllerDe // Media Action - public func mediaDidTap() { + open func mediaDidTap() { let content = bindedMessage!.content as! ACDocumentContent if let fileSource = content.getSource() as? ACFileRemoteSource { - Actor.requestStateWithFileId(fileSource.getFileReference().getFileId(), withCallback: AAFileCallback( + Actor.requestState(withFileId: fileSource.getFileReference().getFileId(), with: AAFileCallback( notDownloaded: { () -> () in - Actor.startDownloadingWithReference(fileSource.getFileReference()) + Actor.startDownloading(with: fileSource.getFileReference()) }, onDownloading: { (progress) -> () in - Actor.cancelDownloadingWithFileId(fileSource.getFileReference().getFileId()) + Actor.cancelDownloading(withFileId: fileSource.getFileReference().getFileId()) }, onDownloaded: { (reference) -> () in if content is ACPhotoContent { if let img = UIImage(contentsOfFile: CocoaFiles.pathFromDescriptor(reference)) { let previewImage = PreviewImage(image: img) let previewController = AAPhotoPreviewController(photo: previewImage, fromView: self.preview) previewController.autoShowBadge = true - self.controller.presentViewController(previewController, animated: true, completion: nil) + self.controller.present(previewController, animated: true, completion: nil) } } else if content is ACVideoContent { self.controller.playVideoFromPath(CocoaFiles.pathFromDescriptor(reference)) } else if self.bindedMessage?.content is ACAnimationContent { if !ActorSDK.sharedActor().isGIFAutoplayEnabled { - if self.playView.hidden { + if self.playView.isHidden { self.preview.stopAnimating() - self.playView.hidden = false + self.playView.isHidden = false } else { self.preview.startAnimating() - self.playView.hidden = true + self.playView.isHidden = true } } } })) } else if let fileSource = content.getSource() as? ACFileLocalSource { let rid = bindedMessage!.rid - Actor.requestUploadStateWithRid(rid, withCallback: AAUploadFileCallback( + Actor.requestUploadState(withRid: rid, with: AAUploadFileCallback( notUploaded: { () -> () in - Actor.resumeUploadWithRid(rid) + Actor.resumeUpload(withRid: rid) }, onUploading: { (progress) -> () in - Actor.pauseUploadWithRid(rid) + Actor.pauseUpload(withRid: rid) }, onUploadedClosure: { () -> () in if content is ACPhotoContent { if let img = UIImage(contentsOfFile: CocoaFiles.pathFromDescriptor(fileSource.getFileDescriptor())) { let previewImage = PreviewImage(image: img) let previewController = AAPhotoPreviewController(photo: previewImage, fromView: self.preview) previewController.autoShowBadge = true - self.controller.presentViewController(previewController, animated: true, completion: nil) + self.controller.present(previewController, animated: true, completion: nil) } } else if content is ACVideoContent { self.controller.playVideoFromPath(CocoaFiles.pathFromDescriptor(fileSource.getFileDescriptor())) } else if self.bindedMessage?.content is ACAnimationContent { if !ActorSDK.sharedActor().isGIFAutoplayEnabled { - if self.playView.hidden { + if self.playView.isHidden { self.preview.stopAnimating() - self.playView.hidden = false + self.playView.isHidden = false } else { self.preview.startAnimating() - self.playView.hidden = true + self.playView.isHidden = true } } } @@ -344,7 +344,7 @@ public class AABubbleMediaCell : AABubbleBaseFileCell, NYTPhotosViewControllerDe // Layouting - public override func layoutContent(maxWidth: CGFloat, offsetX: CGFloat) { + open override func layoutContent(_ maxWidth: CGFloat, offsetX: CGFloat) { let insets = fullContentInsets let contentWidth = self.contentView.frame.width _ = self.contentView.frame.height @@ -354,52 +354,52 @@ public class AABubbleMediaCell : AABubbleBaseFileCell, NYTPhotosViewControllerDe layoutBubble(bubbleWidth, contentHeight: bubbleHeight) if (isOut) { - preview.frame = CGRectMake(contentWidth - insets.left - bubbleWidth, insets.top, bubbleWidth, bubbleHeight) + preview.frame = CGRect(x: contentWidth - insets.left - bubbleWidth, y: insets.top, width: bubbleWidth, height: bubbleHeight) } else { - preview.frame = CGRectMake(insets.left, insets.top, bubbleWidth, bubbleHeight) + preview.frame = CGRect(x: insets.left, y: insets.top, width: bubbleWidth, height: bubbleHeight) } playView.centerIn(preview.frame) - progress.frame = CGRectMake(preview.frame.origin.x + preview.frame.width/2 - 32, preview.frame.origin.y + preview.frame.height/2 - 32, 64, 64) + progress.frame = CGRect(x: preview.frame.origin.x + preview.frame.width/2 - 32, y: preview.frame.origin.y + preview.frame.height/2 - 32, width: 64, height: 64) - timeLabel.frame = CGRectMake(0, 0, 1000, 1000) + timeLabel.frame = CGRect(x: 0, y: 0, width: 1000, height: 1000) timeLabel.sizeToFit() let timeWidth = (isOut ? 23 : 0) + timeLabel.bounds.width let timeHeight: CGFloat = 20 - timeLabel.frame = CGRectMake(preview.frame.maxX - timeWidth - 8, preview.frame.maxY - timeHeight - 4, timeLabel.frame.width, timeHeight) + timeLabel.frame = CGRect(x: preview.frame.maxX - timeWidth - 8, y: preview.frame.maxY - timeHeight - 4, width: timeLabel.frame.width, height: timeHeight) if (isOut) { - statusView.frame = CGRectMake(timeLabel.frame.maxX, timeLabel.frame.minY, 23, timeHeight) + statusView.frame = CGRect(x: timeLabel.frame.maxX, y: timeLabel.frame.minY, width: 23, height: timeHeight) } - timeBg.frame = CGRectMake(timeLabel.frame.minX - 6, timeLabel.frame.minY, timeWidth + 10, timeHeight) + timeBg.frame = CGRect(x: timeLabel.frame.minX - 6, y: timeLabel.frame.minY, width: timeWidth + 10, height: timeHeight) } // Photo preview - public func photosViewController(photosViewController: NYTPhotosViewController, referenceViewForPhoto photo: NYTPhoto) -> UIView? { + open func photosViewController(_ photosViewController: NYTPhotosViewController, referenceViewFor photo: NYTPhoto) -> UIView? { return self.preview } - public func photosViewControllerWillDismiss(photosViewController: NYTPhotosViewController) { + open func photosViewControllerWillDismiss(_ photosViewController: NYTPhotosViewController) { // (UIApplication.sharedApplication().delegate as! AppDelegate).showBadge() - UIApplication.sharedApplication().setStatusBarHidden(false, withAnimation: UIStatusBarAnimation.Fade) + UIApplication.shared.setStatusBarHidden(false, with: UIStatusBarAnimation.fade) } } /** Media cell layout */ -public class MediaCellLayout: AACellLayout { +open class MediaCellLayout: AACellLayout { - public let fastThumb: NSData? - public let contentSize: CGSize - public let screenSize: CGSize - public let autoDownload: Bool - public let duration: Int? + open let fastThumb: Data? + open let contentSize: CGSize + open let screenSize: CGSize + open let autoDownload: Bool + open let duration: Int? /** Creting layout for media bubble @@ -410,7 +410,7 @@ public class MediaCellLayout: AACellLayout { self.duration = duration // Saving content size - self.contentSize = CGSizeMake(width, height) + self.contentSize = CGSize(width: width, height: height) // Saving autodownload flag self.autoDownload = autoDownload @@ -465,9 +465,9 @@ public class MediaCellLayout: AACellLayout { /** Layouter for media bubbles */ -public class AABubbleMediaCellLayouter: AABubbleLayouter { +open class AABubbleMediaCellLayouter: AABubbleLayouter { - public func isSuitable(message: ACMessage) -> Bool { + open func isSuitable(_ message: ACMessage) -> Bool { if message.content is ACPhotoContent { return true } @@ -480,11 +480,11 @@ public class AABubbleMediaCellLayouter: AABubbleLayouter { return false } - public func buildLayout(peer: ACPeer, message: ACMessage) -> AACellLayout { + open func buildLayout(_ peer: ACPeer, message: ACMessage) -> AACellLayout { return MediaCellLayout(message: message, layouter: self) } - public func cellClass() -> AnyClass { + open func cellClass() -> AnyClass { return AABubbleMediaCell.self } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleServiceCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleServiceCell.swift index e79bec0b6c..ae194d6b83 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleServiceCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleServiceCell.swift @@ -4,25 +4,25 @@ import Foundation -public class AABubbleServiceCell : AABubbleCell { +open class AABubbleServiceCell : AABubbleCell { - private static let serviceBubbleFont = UIFont.boldSystemFontOfSize(12) - private static let maxServiceTextWidth: CGFloat = 260 + fileprivate static let serviceBubbleFont = UIFont.boldSystemFont(ofSize: 12) + fileprivate static let maxServiceTextWidth: CGFloat = 260 - private let serviceText = YYLabel() + fileprivate let serviceText = YYLabel() - private var bindedLayout: ServiceCellLayout! + fileprivate var bindedLayout: ServiceCellLayout! public init(frame: CGRect) { super.init(frame: frame, isFullSize: true) // Configuring service label serviceText.font = AABubbleServiceCell.serviceBubbleFont; - serviceText.lineBreakMode = .ByWordWrapping; + serviceText.lineBreakMode = .byWordWrapping; serviceText.numberOfLines = 0; serviceText.textColor = appStyle.chatServiceTextColor - serviceText.contentMode = UIViewContentMode.Center - serviceText.textAlignment = NSTextAlignment.Center + serviceText.contentMode = UIViewContentMode.center + serviceText.textAlignment = NSTextAlignment.center contentView.addSubview(serviceText) // Setting content and bubble insets @@ -30,14 +30,14 @@ public class AABubbleServiceCell : AABubbleCell { bubbleInsets = UIEdgeInsets(top: 3, left: 0, bottom: 3, right: 0) // Setting bubble background - bindBubbleType(.Service, isCompact: false) + bindBubbleType(.service, isCompact: false) } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public override func bind(message: ACMessage, receiveDate: jlong, readDate: jlong, reuse: Bool, cellLayout: AACellLayout, setting: AACellSetting) { + open override func bind(_ message: ACMessage, receiveDate: jlong, readDate: jlong, reuse: Bool, cellLayout: AACellLayout, setting: AACellSetting) { self.bindedLayout = cellLayout as! ServiceCellLayout if (!reuse) { @@ -45,20 +45,20 @@ public class AABubbleServiceCell : AABubbleCell { } } - public override func layoutContent(maxWidth: CGFloat, offsetX: CGFloat) { + open override func layoutContent(_ maxWidth: CGFloat, offsetX: CGFloat) { let insets = fullContentInsets let contentWidth = self.contentView.frame.width let serviceWidth = bindedLayout.textSize.width let serviceHeight = bindedLayout.textSize.height - serviceText.frame = CGRectMake((contentWidth - serviceWidth) / 2.0, insets.top, serviceWidth, serviceHeight); + serviceText.frame = CGRect(x: (contentWidth - serviceWidth) / 2.0, y: insets.top, width: serviceWidth, height: serviceHeight); layoutBubble(serviceWidth, contentHeight: serviceHeight) } } -public class ServiceCellLayout: AACellLayout { +open class ServiceCellLayout: AACellLayout { var text: String var textSize: CGSize @@ -76,19 +76,27 @@ public class ServiceCellLayout: AACellLayout { } } -public class AABubbleServiceCellLayouter: AABubbleLayouter { +open class AABubbleServiceCellLayouter: AABubbleLayouter { - public func isSuitable(message: ACMessage) -> Bool { + open func isSuitable(_ message: ACMessage) -> Bool { return message.content is ACServiceContent } - public func buildLayout(peer: ACPeer, message: ACMessage) -> AACellLayout { - let serviceText = Actor.getFormatter().formatFullServiceMessageWithSenderId(message.senderId, withContent: message.content as! ACServiceContent) + open func buildLayout(_ peer: ACPeer, message: ACMessage) -> AACellLayout { - return ServiceCellLayout(text: serviceText, date: Int64(message.date), layouter: self) + let isChannel: Bool + if peer.isGroup { + isChannel = Actor.getGroupWithGid(peer.peerId).groupType == ACGroupType.channel() + } else { + isChannel = false + } + + let serviceText = Actor.getFormatter().formatFullServiceMessage(withSenderId: message.senderId, with: message.content as! ACServiceContent, withIsChannel: isChannel) + + return ServiceCellLayout(text: serviceText!, date: Int64(message.date), layouter: self) } - public func cellClass() -> AnyClass { + open func cellClass() -> AnyClass { return AABubbleServiceCell.self } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleStickerCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleStickerCell.swift index e6449f9271..ba714a00d6 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleStickerCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleStickerCell.swift @@ -6,7 +6,7 @@ import UIKit import VBFPopFlatButton import YYImage -public class AABubbleStickerCell: AABubbleBaseFileCell { +open class AABubbleStickerCell: AABubbleBaseFileCell { // Views @@ -20,7 +20,7 @@ public class AABubbleStickerCell: AABubbleBaseFileCell { var bindedLayout: StikerCellLayout! var contentLoaded = false - private var callback: AAFileCallback? = nil + fileprivate var callback: AAFileCallback? = nil // Constructors @@ -29,12 +29,12 @@ public class AABubbleStickerCell: AABubbleBaseFileCell { timeBg.image = ActorSDK.sharedActor().style.statusBackgroundImage - timeLabel.font = UIFont.italicSystemFontOfSize(11) + timeLabel.font = UIFont.italicSystemFont(ofSize: 11) timeLabel.textColor = appStyle.chatMediaDateColor - statusView.contentMode = UIViewContentMode.Center + statusView.contentMode = UIViewContentMode.center - preview.contentMode = .ScaleAspectFit + preview.contentMode = .scaleAspectFit contentView.addSubview(preview) @@ -43,7 +43,7 @@ public class AABubbleStickerCell: AABubbleBaseFileCell { contentView.addSubview(statusView) preview.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(AABubbleStickerCell.mediaDidTap))) - preview.userInteractionEnabled = true + preview.isUserInteractionEnabled = true contentInsets = UIEdgeInsets(top: 1, left: 1, bottom: 1, right: 1) } @@ -54,7 +54,7 @@ public class AABubbleStickerCell: AABubbleBaseFileCell { // Binding - public override func bind(message: ACMessage, receiveDate: jlong, readDate: jlong, reuse: Bool, cellLayout: AACellLayout, setting: AACellSetting) { + open override func bind(_ message: ACMessage, receiveDate: jlong, readDate: jlong, reuse: Bool, cellLayout: AACellLayout, setting: AACellSetting) { self.bindedLayout = cellLayout as! StikerCellLayout bubbleInsets = UIEdgeInsets( @@ -66,7 +66,7 @@ public class AABubbleStickerCell: AABubbleBaseFileCell { if (!reuse) { - bindBubbleType(BubbleType.Sticker, isCompact: false) + bindBubbleType(BubbleType.sticker, isCompact: false) // Reset content state preview.image = nil @@ -82,7 +82,7 @@ public class AABubbleStickerCell: AABubbleBaseFileCell { // Update status if (isOut) { - statusView.hidden = false + statusView.isHidden = false switch(message.messageState.toNSEnum()) { case .SENT: if message.sortDate <= readDate { @@ -106,13 +106,13 @@ public class AABubbleStickerCell: AABubbleBaseFileCell { break; } } else { - statusView.hidden = true + statusView.isHidden = true } } // File state binding - public override func fileStateChanged(reference: String?, progress: Int?, isPaused: Bool, isUploading: Bool, selfGeneration: Int) { + open override func fileStateChanged(_ reference: String?, progress: Int?, isPaused: Bool, isUploading: Bool, selfGeneration: Int) { if let r = reference { if (contentLoaded) { return @@ -133,14 +133,14 @@ public class AABubbleStickerCell: AABubbleBaseFileCell { // Media Action - public func mediaDidTap() { + open func mediaDidTap() { } // Layouting - public override func layoutContent(maxWidth: CGFloat, offsetX: CGFloat) { + open override func layoutContent(_ maxWidth: CGFloat, offsetX: CGFloat) { let insets = fullContentInsets let contentWidth = self.contentView.frame.width _ = self.contentView.frame.height @@ -150,26 +150,26 @@ public class AABubbleStickerCell: AABubbleBaseFileCell { layoutBubble(bubbleWidth, contentHeight: bubbleHeight) if (isOut) { - preview.frame = CGRectMake(contentWidth - insets.left - bubbleWidth, insets.top, bubbleWidth, bubbleHeight) + preview.frame = CGRect(x: contentWidth - insets.left - bubbleWidth, y: insets.top, width: bubbleWidth, height: bubbleHeight) } else { - preview.frame = CGRectMake(insets.left, insets.top, bubbleWidth, bubbleHeight) + preview.frame = CGRect(x: insets.left, y: insets.top, width: bubbleWidth, height: bubbleHeight) } //progress.frame = CGRectMake(preview.frame.origin.x + preview.frame.width/2 - 32, preview.frame.origin.y + preview.frame.height/2 - 32, 64, 64) - timeLabel.frame = CGRectMake(0, 0, 1000, 1000) + timeLabel.frame = CGRect(x: 0, y: 0, width: 1000, height: 1000) timeLabel.sizeToFit() let timeWidth = (isOut ? 23 : 0) + timeLabel.bounds.width let timeHeight: CGFloat = 20 - timeLabel.frame = CGRectMake(preview.frame.maxX - timeWidth - 18, preview.frame.maxY - timeHeight - 6, timeLabel.frame.width, timeHeight) + timeLabel.frame = CGRect(x: preview.frame.maxX - timeWidth - 18, y: preview.frame.maxY - timeHeight - 6, width: timeLabel.frame.width, height: timeHeight) if (isOut) { - statusView.frame = CGRectMake(timeLabel.frame.maxX, timeLabel.frame.minY, 23, timeHeight) + statusView.frame = CGRect(x: timeLabel.frame.maxX, y: timeLabel.frame.minY, width: 23, height: timeHeight) } - timeBg.frame = CGRectMake(timeLabel.frame.minX - 4, timeLabel.frame.minY - 1, timeWidth + 8, timeHeight + 2) + timeBg.frame = CGRect(x: timeLabel.frame.minX - 4, y: timeLabel.frame.minY - 1, width: timeWidth + 8, height: timeHeight + 2) } } @@ -177,12 +177,12 @@ public class AABubbleStickerCell: AABubbleBaseFileCell { /** Media cell layout */ -public class StikerCellLayout: AACellLayout { +open class StikerCellLayout: AACellLayout { // public let fastThumb: NSData? - public let contentSize: CGSize - public let screenSize: CGSize - public let autoDownload: Bool + open let contentSize: CGSize + open let screenSize: CGSize + open let autoDownload: Bool /** Creting layout for media bubble @@ -190,7 +190,7 @@ public class StikerCellLayout: AACellLayout { public init(id: Int64, width: CGFloat, height:CGFloat, date: Int64, stickerContent: ACStickerContent?, autoDownload: Bool, layouter: AABubbleLayouter) { // Saving content size - self.contentSize = CGSizeMake(width, height) + self.contentSize = CGSize(width: width, height: height) // Saving autodownload flag self.autoDownload = autoDownload @@ -228,9 +228,9 @@ public class StikerCellLayout: AACellLayout { /** Layouter for media bubbles */ -public class AABubbleStickerCellLayouter: AABubbleLayouter { +open class AABubbleStickerCellLayouter: AABubbleLayouter { - public func isSuitable(message: ACMessage) -> Bool { + open func isSuitable(_ message: ACMessage) -> Bool { if message.content is ACStickerContent { return true } @@ -238,11 +238,11 @@ public class AABubbleStickerCellLayouter: AABubbleLayouter { return false } - public func buildLayout(peer: ACPeer, message: ACMessage) -> AACellLayout { + open func buildLayout(_ peer: ACPeer, message: ACMessage) -> AACellLayout { return StikerCellLayout(message: message, layouter: self) } - public func cellClass() -> AnyClass { + open func cellClass() -> AnyClass { return AABubbleStickerCell.self } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleTextCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleTextCell.swift index 66c3a70805..e1e465093c 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleTextCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleTextCell.swift @@ -6,7 +6,7 @@ import Foundation import UIKit import YYImage -public class AABubbleTextCell : AABubbleCell { +open class AABubbleTextCell : AABubbleCell { // TODO: Better max width calculations @@ -15,25 +15,25 @@ public class AABubbleTextCell : AABubbleCell { static let fontItalic = UIFont.italicTextFontOfSize(fontSize) static let fontBold = UIFont.boldTextFontOfSize(fontSize) - private static let dateFont = UIFont.italicSystemFontOfSize(11) - private static let senderFont = UIFont.boldSystemFontOfSize(15) + fileprivate static let dateFont = UIFont.italicSystemFont(ofSize: 11) + fileprivate static let senderFont = UIFont.boldSystemFont(ofSize: 15) static let bubbleFont = fontRegular static let bubbleFontUnsupported = fontItalic static let senderHeight = CGFloat(20) - private let messageText = YYLabel() - private let senderNameLabel = YYLabel() - private let dateText = YYLabel() - private let statusView = UIImageView() + fileprivate let messageText = YYLabel() + fileprivate let senderNameLabel = YYLabel() + fileprivate let dateText = YYLabel() + fileprivate let statusView = UIImageView() - private var needRelayout = true - private var isClanchTop:Bool = false - private var isClanchBottom:Bool = false + fileprivate var needRelayout = true + fileprivate var isClanchTop:Bool = false + fileprivate var isClanchBottom:Bool = false - private var dateWidth: CGFloat = 0 + fileprivate var dateWidth: CGFloat = 0 - private var cellLayout: TextCellLayout! + fileprivate var cellLayout: TextCellLayout! public init(frame: CGRect) { super.init(frame: frame, isFullSize: false) @@ -44,20 +44,20 @@ public class AABubbleTextCell : AABubbleCell { messageText.clearContentsBeforeAsynchronouslyDisplay = true messageText.highlightTapAction = { (containerView: UIView, text: NSAttributedString, range: NSRange, rect: CGRect) -> () in - let attributes = text.attributesAtIndex(range.location, effectiveRange: nil) + let attributes = text.attributes(at: range.location, effectiveRange: nil) if let attrs = attributes["YYTextHighlight"] as? YYTextHighlight { if let url = attrs.userInfo!["url"] as? String { - self.openUrl(NSURL(string: url)!) + self.openUrl(URL(string: url)!) } } } messageText.highlightLongPressAction = { (containerView: UIView, text: NSAttributedString, range: NSRange, rect: CGRect) -> () in self.bubble - let attributes = text.attributesAtIndex(range.location, effectiveRange: nil) + let attributes = text.attributes(at: range.location, effectiveRange: nil) if let attrs = attributes["YYTextHighlight"] as? YYTextHighlight { if let url = attrs.userInfo!["url"] as? String { - self.urlLongTap(NSURL(string: url)!) + self.urlLongTap(URL(string: url)!) } } } @@ -78,7 +78,7 @@ public class AABubbleTextCell : AABubbleCell { // dateText.numberOfLines = 1 // dateText.textAlignment = .Right - statusView.contentMode = UIViewContentMode.Center + statusView.contentMode = UIViewContentMode.center contentView.addSubview(messageText) contentView.addSubview(dateText) @@ -92,7 +92,7 @@ public class AABubbleTextCell : AABubbleCell { // Data binding - public override func bind(message: ACMessage, receiveDate: jlong, readDate: jlong, reuse: Bool, cellLayout: AACellLayout, setting: AACellSetting) { + open override func bind(_ message: ACMessage, receiveDate: jlong, readDate: jlong, reuse: Bool, cellLayout: AACellLayout, setting: AACellSetting) { // Saving cell settings self.cellLayout = cellLayout as! TextCellLayout @@ -109,17 +109,17 @@ public class AABubbleTextCell : AABubbleCell { // Setting sender name if needed if isGroup && !isOut { - senderNameLabel.hidden = false + senderNameLabel.isHidden = false senderNameLabel.textLayout = self.cellLayout.senderLayout } else { - senderNameLabel.hidden = true + senderNameLabel.isHidden = true senderNameLabel.textLayout = nil } } // Always update bubble insets if (isOut) { - bindBubbleType(.TextOut, isCompact: isClanchBottom) + bindBubbleType(.textOut, isCompact: isClanchBottom) bubbleInsets = UIEdgeInsets( top: (isClanchTop ? AABubbleCell.bubbleTopCompact : AABubbleCell.bubbleTop), @@ -132,7 +132,7 @@ public class AABubbleTextCell : AABubbleCell { bottom: AABubbleCell.bubbleContentBottom, right: (isClanchBottom ? 4 : 10)) } else { - bindBubbleType(.TextIn, isCompact: isClanchBottom) + bindBubbleType(.textIn, isCompact: isClanchBottom) // dateText.textColor = appStyle.chatTextDateInColor bubbleInsets = UIEdgeInsets( @@ -182,45 +182,45 @@ public class AABubbleTextCell : AABubbleCell { // Menu for Text cell - public override func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool { - if action == #selector(NSObject.copy(_:)) { - if (bindedMessage!.content is ACTextContent) { - return true - } - } - if action == #selector(NSObject.delete(_:)) { - return true - } - return false - } - - public override func copy(sender: AnyObject?) { - UIPasteboard.generalPasteboard().string = (bindedMessage!.content as! ACTextContent).text - } +// open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { +// if action == #selector(NSObject.copy(_:)) { +// if (bindedMessage!.content is ACTextContent) { +// return true +// } +// } +// if action == #selector(NSObject.delete(_:)) { +// return true +// } +// return false +// } +// +// open override func copy(_ sender: AnyObject?) { +// UIPasteboard.general.string = (bindedMessage!.content as! ACTextContent).text +// } - public func urlLongTap(url: NSURL) { + open func urlLongTap(_ url: URL) { if url.scheme != "source" && url.scheme == "send" { - let actionSheet: UIAlertController = UIAlertController(title: nil, message: url.absoluteString, preferredStyle: .ActionSheet) - actionSheet.addAction(UIAlertAction(title: AALocalized("ActionOpenLink"), style: .Default, handler: { action in + let actionSheet: UIAlertController = UIAlertController(title: nil, message: url.absoluteString, preferredStyle: .actionSheet) + actionSheet.addAction(UIAlertAction(title: AALocalized("ActionOpenLink"), style: .default, handler: { action in self.openUrl(url) })) - actionSheet.addAction(UIAlertAction(title: AALocalized("ActionCopyLink"), style: .Default, handler: { action in - UIPasteboard.generalPasteboard().string = url.absoluteString + actionSheet.addAction(UIAlertAction(title: AALocalized("ActionCopyLink"), style: .default, handler: { action in + UIPasteboard.general.string = url.absoluteString self.controller.alertUser("AlertLinkCopied") })) - actionSheet.addAction(UIAlertAction(title: AALocalized("ActionCancel"), style: .Cancel, handler:nil)) - self.controller.presentViewController(actionSheet, animated: true, completion: nil) + actionSheet.addAction(UIAlertAction(title: AALocalized("ActionCancel"), style: .cancel, handler:nil)) + self.controller.present(actionSheet, animated: true, completion: nil) } } - public func openUrl(url: NSURL) { + open func openUrl(_ url: URL) { if url.scheme == "source" { - let path = url.path! - let index = Int(path.substringFromIndex(path.startIndex.advancedBy(1)))! + let path = url.path + let index = Int(path.substring(from: path.characters.index(path.startIndex, offsetBy: 1)))! let code = self.cellLayout.sources[index] self.controller.navigateNext(AACodePreviewController(code: code), removeCurrent: false) } else if url.scheme == "send" { - Actor.sendMessageWithPeer(self.peer, withText: url.absoluteString.skip(5)) + Actor.sendMessage(with: self.peer, withText: url.absoluteString.skip(5)) } else { ActorSDK.sharedActor().openUrl(url.absoluteString) } @@ -228,7 +228,7 @@ public class AABubbleTextCell : AABubbleCell { // Layouting - public override func layoutContent(maxWidth: CGFloat, offsetX: CGFloat) { + open override func layoutContent(_ maxWidth: CGFloat, offsetX: CGFloat) { // Convenience let insets = fullContentInsets @@ -237,18 +237,18 @@ public class AABubbleTextCell : AABubbleCell { let bubbleWidth = round(self.cellLayout.bubbleSize.width) let bubbleHeight = round(self.cellLayout.bubbleSize.height) - self.messageText.frame = CGRectMake(0, 0, textSize.width, textSize.height) + self.messageText.frame = CGRect(x: 0, y: 0, width: textSize.width, height: textSize.height) // Layout elements if (self.isOut) { self.messageText.frame.origin = CGPoint(x: contentWidth - bubbleWidth - insets.right, y: insets.top /*+ topPadding*/) - self.dateText.frame = CGRectMake(contentWidth - insets.right - 70 + 46 - dateWidth, bubbleHeight + insets.top - 20, dateWidth, 26) - self.statusView.frame = CGRectMake(contentWidth - insets.right - 24, bubbleHeight + insets.top - 20, 20, 26) - self.statusView.hidden = false + self.dateText.frame = CGRect(x: contentWidth - insets.right - 70 + 46 - dateWidth, y: bubbleHeight + insets.top - 20, width: dateWidth, height: 26) + self.statusView.frame = CGRect(x: contentWidth - insets.right - 24, y: bubbleHeight + insets.top - 20, width: 20, height: 26) + self.statusView.isHidden = false } else { self.messageText.frame.origin = CGPoint(x: insets.left, y: insets.top/* + topPadding*/) - self.dateText.frame = CGRectMake(insets.left + bubbleWidth - 47 + 46 - dateWidth, bubbleHeight + insets.top - 20, dateWidth, 26) - self.statusView.hidden = true + self.dateText.frame = CGRect(x: insets.left + bubbleWidth - 47 + 46 - dateWidth, y: bubbleHeight + insets.top - 20, width: dateWidth, height: 26) + self.statusView.isHidden = true } if self.isGroup && !self.isOut { @@ -262,25 +262,25 @@ public class AABubbleTextCell : AABubbleCell { /** Text cell layout */ -public class TextCellLayout: AACellLayout { +open class TextCellLayout: AACellLayout { - private class func maxTextWidth(isOut: Bool, peer: ACPeer) -> CGFloat { + fileprivate class func maxTextWidth(_ isOut: Bool, peer: ACPeer) -> CGFloat { if AADevice.isiPad { return 400 } else { if peer.isGroup { if isOut { - return UIScreen.mainScreen().bounds.width - 110 + return UIScreen.main.bounds.width - 110 } else { - return UIScreen.mainScreen().bounds.width - 90 + return UIScreen.main.bounds.width - 90 } } else { - return UIScreen.mainScreen().bounds.width - 40 + return UIScreen.main.bounds.width - 40 } } } - private class func timeWidth(isOut: Bool) -> CGFloat { + fileprivate class func timeWidth(_ isOut: Bool) -> CGFloat { if isOut { return 60 } else { @@ -288,12 +288,12 @@ public class TextCellLayout: AACellLayout { } } - private static let textKey = "text" - private static let unsupportedKey = "unsupported" + fileprivate static let textKey = "text" + fileprivate static let unsupportedKey = "unsupported" - private static let stringOutPadding = " " + ("_".repeatString(7)); - private static let stringInPadding = " " + ("_".repeatString(4)); - private static let parser = ARMarkdownParser(int: ARMarkdownParser_MODE_FULL) + fileprivate static let stringOutPadding = " " + ("_".repeatString(7)); + fileprivate static let stringInPadding = " " + ("_".repeatString(4)); + fileprivate static let parser = ARMarkdownParser(int: ARMarkdownParser_MODE_FULL) var text: String var attrText: NSAttributedString @@ -320,7 +320,7 @@ public class TextCellLayout: AACellLayout { let maxTextWidth = TextCellLayout.maxTextWidth(isOut, peer: peer) let timeWidth = TextCellLayout.timeWidth(isOut) - let container = YYTextContainer(size: CGSizeMake(maxTextWidth, CGFloat.max)) + let container = YYTextContainer(size: CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude)) textLayout = YYTextLayout(container: container, text: attributedText)! @@ -380,13 +380,13 @@ public class TextCellLayout: AACellLayout { let attrDate = NSMutableAttributedString(string: AACellLayout.formatDate(date)) attrDate.yy_font = AABubbleTextCell.dateFont attrDate.yy_color = ActorSDK.sharedActor().style.chatTextDateOutColor - dateLayout = YYTextLayout(containerSize: CGSizeMake(timeWidth, CGFloat.max), text: attrDate) + dateLayout = YYTextLayout(containerSize: CGSize(width: timeWidth, height: CGFloat.greatestFiniteMagnitude), text: attrDate) dateWidth = dateLayout?.textBoundingSize.width } else { let attrDate = NSMutableAttributedString(string: AACellLayout.formatDate(date)) attrDate.yy_font = AABubbleTextCell.dateFont attrDate.yy_color = ActorSDK.sharedActor().style.chatTextDateInColor - dateLayout = YYTextLayout(containerSize: CGSizeMake(timeWidth, CGFloat.max), text: attrDate) + dateLayout = YYTextLayout(containerSize: CGSize(width: timeWidth, height: CGFloat.greatestFiniteMagnitude), text: attrDate) dateWidth = dateLayout?.textBoundingSize.width } @@ -485,17 +485,17 @@ public class TextCellLayout: AACellLayout { /** Text cell layouter */ -public class AABubbleTextCellLayouter: AABubbleLayouter { +open class AABubbleTextCellLayouter: AABubbleLayouter { - public func buildLayout(peer: ACPeer, message: ACMessage) -> AACellLayout { + open func buildLayout(_ peer: ACPeer, message: ACMessage) -> AACellLayout { return TextCellLayout(message: message, peer: peer, layouter: self) } - public func isSuitable(message: ACMessage) -> Bool { + open func isSuitable(_ message: ACMessage) -> Bool { return message.content is ACTextContent } - public func cellClass() -> AnyClass { + open func cellClass() -> AnyClass { return AABubbleTextCell.self } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleVoiceCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleVoiceCell.swift index 2c0318ca6d..0408ce3609 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleVoiceCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Cell/AABubbleVoiceCell.swift @@ -6,20 +6,20 @@ import UIKit import VBFPopFlatButton import YYImage -public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPlayerDelegate,AAModernViewInlineMediaContextDelegate { +open class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPlayerDelegate,AAModernViewInlineMediaContextDelegate { // Views - private let progress = AAProgressView(size: CGSizeMake(44, 44)) - private let timeLabel = AttributedLabel() - private let voiceTimeLabel = AttributedLabel() - private let playPauseButton = UIButton() - private let soundProgressSlider = UISlider() - private let statusView = UIImageView() + fileprivate let progress = AAProgressView(size: CGSize(width: 44, height: 44)) + fileprivate let timeLabel = AttributedLabel() + fileprivate let voiceTimeLabel = AttributedLabel() + fileprivate let playPauseButton = UIButton() + fileprivate let soundProgressSlider = UISlider() + fileprivate let statusView = UIImageView() - private let durationLabel = AttributedLabel() - private let titleLabel = AttributedLabel() + fileprivate let durationLabel = AttributedLabel() + fileprivate let titleLabel = AttributedLabel() // Binded data @@ -35,42 +35,42 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl //////////////////////////////////////////////////// - timeLabel.font = UIFont.italicSystemFontOfSize(11) + timeLabel.font = UIFont.italicSystemFont(ofSize: 11) timeLabel.textColor = appStyle.chatTextDateOutColor //////////////////////////////////////////////////// - statusView.contentMode = UIViewContentMode.Center + statusView.contentMode = UIViewContentMode.center //////////////////////////////////////////////////// soundProgressSlider.tintColor = appStyle.chatStatusSending - soundProgressSlider.userInteractionEnabled = false + soundProgressSlider.isUserInteractionEnabled = false //soundProgressSlider.addTarget(self, action: "seekToNewAudioValue", forControlEvents: UIControlEvents.ValueChanged) let insets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) let trackLeftImage = UIImage.tinted("aa_voiceplaybackground", color: UIColor(red: 0.0, green: 0.761, blue: 0.9964, alpha: 1.0 )) - let trackLeftResizable = trackLeftImage.resizableImageWithCapInsets(insets) - soundProgressSlider.setMinimumTrackImage(trackLeftResizable, forState: .Normal) + let trackLeftResizable = trackLeftImage.resizableImage(withCapInsets: insets) + soundProgressSlider.setMinimumTrackImage(trackLeftResizable, for: UIControlState()) let trackRightImage = UIImage.tinted("aa_voiceplaybackground", color: UIColor(red: 0.0, green: 0.5856, blue: 0.9985, alpha: 1.0 )) - let trackRightResizable = trackRightImage.resizableImageWithCapInsets(insets) - soundProgressSlider.setMaximumTrackImage(trackRightResizable, forState: .Normal) + let trackRightResizable = trackRightImage.resizableImage(withCapInsets: insets) + soundProgressSlider.setMaximumTrackImage(trackRightResizable, for: UIControlState()) let thumbImageNormal = UIImage.bundled("aa_thumbvoiceslider") - soundProgressSlider.setThumbImage(thumbImageNormal, forState: .Normal) + soundProgressSlider.setThumbImage(thumbImageNormal, for: UIControlState()) //////////////////////////////////////////////////// - voiceTimeLabel.font = UIFont.italicSystemFontOfSize(11) + voiceTimeLabel.font = UIFont.italicSystemFont(ofSize: 11) voiceTimeLabel.textColor = appStyle.chatTextDateOutColor //////////////////////////////////////////////////// - durationLabel.font = UIFont.systemFontOfSize(11.0) + durationLabel.font = UIFont.systemFont(ofSize: 11.0) durationLabel.textColor = appStyle.chatTextDateOutColor durationLabel.text = " " durationLabel.sizeToFit() @@ -97,8 +97,8 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl //////////////////////////////////////////////////// - playPauseButton.setImage(UIImage.bundled("aa_playrecordbutton"), forState: UIControlState.Normal) - playPauseButton.addTarget(self, action: #selector(AABubbleVoiceCell.mediaDidTap), forControlEvents: UIControlEvents.TouchUpInside) + playPauseButton.setImage(UIImage.bundled("aa_playrecordbutton"), for: UIControlState()) + playPauseButton.addTarget(self, action: #selector(AABubbleVoiceCell.mediaDidTap), for: UIControlEvents.touchUpInside) contentInsets = UIEdgeInsets(top: 1, left: 1, bottom: 1, right: 1) @@ -110,7 +110,7 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl // MARK: - Binding - public override func bind(message: ACMessage, receiveDate: jlong, readDate: jlong, reuse: Bool, cellLayout: AACellLayout, setting: AACellSetting) { + open override func bind(_ message: ACMessage, receiveDate: jlong, readDate: jlong, reuse: Bool, cellLayout: AACellLayout, setting: AACellSetting) { self.bindedLayout = cellLayout as! VoiceMessageCellLayout let document = message.content as! ACVoiceContent @@ -126,9 +126,9 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl // Bind bubble if (self.isOut) { - bindBubbleType(BubbleType.MediaOut, isCompact: false) + bindBubbleType(BubbleType.mediaOut, isCompact: false) } else { - bindBubbleType(BubbleType.MediaIn, isCompact: false) + bindBubbleType(BubbleType.mediaIn, isCompact: false) } titleLabel.text = AALocalized("ChatVoiceMessage") @@ -136,8 +136,8 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl // Reset progress self.progress.hideButton() - UIView.animateWithDuration(0, animations: { () -> Void in - self.progress.hidden = true + UIView.animate(withDuration: 0, animations: { () -> Void in + self.progress.isHidden = true }) // Bind file @@ -162,16 +162,16 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl if self.controller.currentAudioFileId != fileID { self.soundProgressSlider.value = 0.0 - self.playPauseButton.setImage(UIImage.bundled("aa_playrecordbutton"), forState: UIControlState.Normal) + self.playPauseButton.setImage(UIImage.bundled("aa_playrecordbutton"), for: UIControlState()) self.controller.voiceContext?.removeDelegate(self) } else { self.controller.voiceContext?.delegate = self self.controller.voicePlayer?.delegate = self if self.controller.voicePlayer?.isPaused() == false { - self.playPauseButton.setImage(UIImage.bundled("aa_pauserecordbutton"), forState: UIControlState.Normal) + self.playPauseButton.setImage(UIImage.bundled("aa_pauserecordbutton"), for: UIControlState()) } else { - self.playPauseButton.setImage(UIImage.bundled("aa_playrecordbutton"), forState: UIControlState.Normal) + self.playPauseButton.setImage(UIImage.bundled("aa_playrecordbutton"), for: UIControlState()) } } @@ -192,7 +192,7 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl // Update status if (isOut) { - statusView.hidden = false + statusView.isHidden = false switch(message.messageState.toNSEnum()) { case .SENT: if message.sortDate <= readDate { @@ -215,14 +215,14 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl break } } else { - statusView.hidden = true + statusView.isHidden = true } } //MARK: - File state binding - public override func fileStateChanged(reference: String?, progress: Int?, isPaused: Bool, isUploading: Bool, selfGeneration: Int) { + open override func fileStateChanged(_ reference: String?, progress: Int?, isPaused: Bool, isUploading: Bool, selfGeneration: Int) { runOnUiThread(selfGeneration) { () -> () in if isUploading { if isPaused { @@ -263,14 +263,14 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl //MARK: - Media Action - public func mediaDidTap() { + open func mediaDidTap() { let content = bindedMessage!.content as! ACDocumentContent if let fileSource = content.getSource() as? ACFileRemoteSource { - Actor.requestStateWithFileId(fileSource.getFileReference().getFileId(), withCallback: AAFileCallback( + Actor.requestState(withFileId: fileSource.getFileReference().getFileId(), with: AAFileCallback( notDownloaded: { () -> () in - Actor.startDownloadingWithReference(fileSource.getFileReference()) + Actor.startDownloading(with: fileSource.getFileReference()) }, onDownloading: { (progress) -> () in - Actor.cancelDownloadingWithFileId(fileSource.getFileReference().getFileId()) + Actor.cancelDownloading(withFileId: fileSource.getFileReference().getFileId()) }, onDownloaded: { (reference) -> () in dispatchOnUi({ () -> Void in @@ -281,7 +281,7 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl self.controller.playVoiceFromPath(path,fileId: fileID,position:self.soundProgressSlider.value) - self.playPauseButton.setImage(UIImage.bundled("aa_pauserecordbutton"), forState: UIControlState.Normal) + self.playPauseButton.setImage(UIImage.bundled("aa_pauserecordbutton"), for: UIControlState()) self.controller.voicePlayer.delegate = self self.controller.voiceContext.delegate = self @@ -292,11 +292,11 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl } else if let fileSource = content.getSource() as? ACFileLocalSource { let rid = bindedMessage!.rid - Actor.requestUploadStateWithRid(rid, withCallback: AAUploadFileCallback( + Actor.requestUploadState(withRid: rid, with: AAUploadFileCallback( notUploaded: { () -> () in - Actor.resumeUploadWithRid(rid) + Actor.resumeUpload(withRid: rid) }, onUploading: { (progress) -> () in - Actor.pauseUploadWithRid(rid) + Actor.pauseUpload(withRid: rid) }, onUploadedClosure: { () -> () in dispatchOnUi({ () -> Void in @@ -309,7 +309,7 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl self.controller.playVoiceFromPath(path,fileId: fileID,position:self.soundProgressSlider.value) - self.playPauseButton.setImage(UIImage.bundled("aa_pauserecordbutton"), forState: UIControlState.Normal) + self.playPauseButton.setImage(UIImage.bundled("aa_pauserecordbutton"), for: UIControlState()) self.controller.voicePlayer.delegate = self self.controller.voiceContext.delegate = self @@ -324,15 +324,15 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl } } - public func seekToNewAudioValue() { + open func seekToNewAudioValue() { } - public func audioPlayerDidFinish() { + open func audioPlayerDidFinish() { dispatchOnUi { () -> Void in - self.playPauseButton.setImage(UIImage.bundled("aa_playrecordbutton"), forState: UIControlState.Normal) + self.playPauseButton.setImage(UIImage.bundled("aa_playrecordbutton"), for: UIControlState()) self.soundProgressSlider.value = 0.0 self.controller.voicesCache[self.controller.currentAudioFileId] = 0.0 @@ -340,14 +340,14 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl } - public func inlineMediaPlaybackStateUpdated(isPaused: Bool, playbackPosition: Float, timestamp: NSTimeInterval, preciseDuration: NSTimeInterval) { + open func inlineMediaPlaybackStateUpdated(_ isPaused: Bool, playbackPosition: Float, timestamp: TimeInterval, preciseDuration: TimeInterval) { dispatchOnUi({ () -> Void in self.soundProgressSlider.value = playbackPosition if (isPaused == true) { - self.playPauseButton.setImage(UIImage.bundled("aa_playrecordbutton"), forState: UIControlState.Normal) + self.playPauseButton.setImage(UIImage.bundled("aa_playrecordbutton"), for: UIControlState()) } self.controller.voicesCache[self.controller.currentAudioFileId] = playbackPosition @@ -359,7 +359,7 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl //MARK: - Layouting - public override func layoutContent(maxWidth: CGFloat, offsetX: CGFloat) { + open override func layoutContent(_ maxWidth: CGFloat, offsetX: CGFloat) { let insets = fullContentInsets let contentWidth = self.contentView.frame.width @@ -370,25 +370,25 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl let top = insets.top - 2 // Progress state - let progressRect = CGRectMake(contentLeft + 7.5, 7.5 + top, 44, 44) + let progressRect = CGRect(x: contentLeft + 7.5, y: 7.5 + top, width: 44, height: 44) self.progress.frame = progressRect self.playPauseButton.frame = progressRect - timeLabel.frame = CGRectMake(0, 0, 1000, 1000) + timeLabel.frame = CGRect(x: 0, y: 0, width: 1000, height: 1000) timeLabel.sizeToFit() // Content - self.soundProgressSlider.frame = CGRectMake(contentLeft + 62, 16 + top, 200 - 70, 22) - self.durationLabel.frame = CGRectMake(contentLeft + 62, 10 + 25 + top, 200 - 64, 22) + self.soundProgressSlider.frame = CGRect(x: contentLeft + 62, y: 16 + top, width: 200 - 70, height: 22) + self.durationLabel.frame = CGRect(x: contentLeft + 62, y: 10 + 25 + top, width: 200 - 64, height: 22) // Message state if (self.isOut) { - self.timeLabel.frame = CGRectMake(self.bubble.frame.maxX - 55 - self.bubblePadding, self.bubble.frame.maxY - 24, 46, 26) - self.statusView.frame = CGRectMake(self.bubble.frame.maxX - 24 - self.bubblePadding, self.bubble.frame.maxY - 24, 20, 26) - self.statusView.hidden = false + self.timeLabel.frame = CGRect(x: self.bubble.frame.maxX - 55 - self.bubblePadding, y: self.bubble.frame.maxY - 24, width: 46, height: 26) + self.statusView.frame = CGRect(x: self.bubble.frame.maxX - 24 - self.bubblePadding, y: self.bubble.frame.maxY - 24, width: 20, height: 26) + self.statusView.isHidden = false } else { - self.timeLabel.frame = CGRectMake(self.bubble.frame.maxX - 47 - self.bubblePadding, self.bubble.frame.maxY - 24, 46, 26) - self.statusView.hidden = true + self.timeLabel.frame = CGRect(x: self.bubble.frame.maxX - 47 - self.bubblePadding, y: self.bubble.frame.maxY - 24, width: 46, height: 26) + self.statusView.isHidden = true } } @@ -398,16 +398,16 @@ public class AABubbleVoiceCell: AABubbleBaseFileCell,AAModernConversationAudioPl /** Voice cell layout */ -public class VoiceMessageCellLayout: AACellLayout { +open class VoiceMessageCellLayout: AACellLayout { - public let contentSize: CGSize - public let screenSize: CGSize - public let autoDownload: Bool + open let contentSize: CGSize + open let screenSize: CGSize + open let autoDownload: Bool - public let fileName: String - public let fileExt: String - public let fileSize: String - public var voiceDuration: String! + open let fileName: String + open let fileExt: String + open let fileSize: String + open var voiceDuration: String! /** Creting layout for media bubble @@ -415,7 +415,7 @@ public class VoiceMessageCellLayout: AACellLayout { public init(fileName: String, fileExt: String, fileSize: Int,id: Int64, date: Int64, autoDownload: Bool,duration:jint, layouter: AABubbleLayouter) { // Saving content size - self.contentSize = CGSizeMake(200, 55) + self.contentSize = CGSize(width: 200, height: 55) // Saving autodownload flag self.autoDownload = autoDownload @@ -424,7 +424,7 @@ public class VoiceMessageCellLayout: AACellLayout { self.screenSize = CGSize(width: 200, height: 55) self.fileName = fileName - self.fileExt = fileExt.lowercaseString + self.fileExt = fileExt.lowercased() self.fileSize = Actor.getFormatter().formatFileSize(jint(fileSize)) // Creating layout @@ -433,7 +433,7 @@ public class VoiceMessageCellLayout: AACellLayout { self.voiceDuration = getTimeString(Int(duration)) } - func getTimeString(totalSeconds:Int) -> String { + func getTimeString(_ totalSeconds:Int) -> String { let seconds = Int(totalSeconds % 60) let minutes = Int((totalSeconds / 60) % 60) @@ -478,9 +478,9 @@ public class VoiceMessageCellLayout: AACellLayout { /** Layouter for voice bubbles */ -public class AABubbleVoiceCellLayouter: AABubbleLayouter { +open class AABubbleVoiceCellLayouter: AABubbleLayouter { - public func isSuitable(message: ACMessage) -> Bool { + open func isSuitable(_ message: ACMessage) -> Bool { if message.content is ACVoiceContent { return true } @@ -488,11 +488,11 @@ public class AABubbleVoiceCellLayouter: AABubbleLayouter { return false } - public func buildLayout(peer: ACPeer, message: ACMessage) -> AACellLayout { + open func buildLayout(_ peer: ACPeer, message: ACMessage) -> AACellLayout { return VoiceMessageCellLayout(message: message, layouter: self) } - public func cellClass() -> AnyClass { + open func cellClass() -> AnyClass { return AABubbleVoiceCell.self } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Layouting/AABubbleBackgroundProcessor.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Layouting/AABubbleBackgroundProcessor.swift index f4fbcd76c1..d243c0ae05 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Layouting/AABubbleBackgroundProcessor.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Layouting/AABubbleBackgroundProcessor.swift @@ -49,7 +49,7 @@ class AAListProcessor: NSObject, ARListProcessor { self.peer = peer } - func processWithItems(items: JavaUtilList, withPrevious previous: AnyObject?) -> AnyObject? { + func process(withItems items: JavaUtilList, withPrevious previous: Any?) -> Any? { var objs = [ACMessage]() var indexes = [jlong: Int]() @@ -74,7 +74,7 @@ class AAListProcessor: NSObject, ARListProcessor { // Building content list and dictionary from rid to index list for i in 0.. AACellSetting { + func buildCellSetting(_ index: Int, items: [ACMessage]) -> AACellSetting { let current = items[index] let id = Int64(current.rid) @@ -240,14 +240,14 @@ class AAListProcessor: NSObject, ARListProcessor { /** Checking if messages have same send day */ - func areSameDate(source:ACMessage, prev: ACMessage) -> Bool { - let calendar = NSCalendar.currentCalendar() + func areSameDate(_ source:ACMessage, prev: ACMessage) -> Bool { + let calendar = Calendar.current - let currentDate = NSDate(timeIntervalSince1970: Double(source.date)/1000.0) - let currentDateComp = calendar.components([.Day, .Year, .Month], fromDate: currentDate) + let currentDate = Date(timeIntervalSince1970: Double(source.date)/1000.0) + let currentDateComp = (calendar as NSCalendar).components([.day, .year, .month], from: currentDate) - let nextDate = NSDate(timeIntervalSince1970: Double(prev.date)/1000.0) - let nextDateComp = calendar.components([.Day, .Year, .Month], fromDate: nextDate) + let nextDate = Date(timeIntervalSince1970: Double(prev.date)/1000.0) + let nextDateComp = (calendar as NSCalendar).components([.day, .year, .month], from: nextDate) return (currentDateComp.year == nextDateComp.year && currentDateComp.month == nextDateComp.month && currentDateComp.day == nextDateComp.day) } @@ -255,7 +255,7 @@ class AAListProcessor: NSObject, ARListProcessor { /** Checking if it is good to make bubbles clenched */ - func useCompact(source: ACMessage, next: ACMessage) -> Bool { + func useCompact(_ source: ACMessage, next: ACMessage) -> Bool { if (source.content is ACServiceContent) { if (next.content is ACServiceContent) { return true @@ -272,7 +272,7 @@ class AAListProcessor: NSObject, ARListProcessor { return false } - func measureHeight(message: ACMessage, setting: AACellSetting, layout: AACellLayout) -> CGFloat { + func measureHeight(_ message: ACMessage, setting: AACellSetting, layout: AACellLayout) -> CGFloat { let content = message.content! @@ -293,7 +293,7 @@ class AAListProcessor: NSObject, ARListProcessor { return height } - func buildLayout(message: ACMessage, layoutCache: AALayoutCache) -> AACellLayout { + func buildLayout(_ message: ACMessage, layoutCache: AALayoutCache) -> AACellLayout { var layout: AACellLayout! = layoutCache.pick(message.rid) @@ -305,4 +305,4 @@ class AAListProcessor: NSObject, ARListProcessor { return layout } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Layouting/AAMessagesFlowLayout.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Layouting/AAMessagesFlowLayout.swift index e1d411ea4c..1dc0a6cc23 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Layouting/AAMessagesFlowLayout.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Layouting/AAMessagesFlowLayout.swift @@ -8,8 +8,8 @@ private let ENABLE_LOGS = false class AAMessagesFlowLayout : UICollectionViewLayout { - var deletedIndexPaths = [NSIndexPath]() - var insertedIndexPaths = [NSIndexPath]() + var deletedIndexPaths = [IndexPath]() + var insertedIndexPaths = [IndexPath]() var items = [AALayoutItem?]() var frames = [CGRect?]() var isLoadMore: Bool = false @@ -29,18 +29,18 @@ class AAMessagesFlowLayout : UICollectionViewLayout { fatalError("init(coder:) has not been implemented") } - func beginUpdates(isLoadMore: Bool, list: AAPreprocessedList?, unread: jlong?) { + func beginUpdates(_ isLoadMore: Bool, list: AAPreprocessedList?, unread: jlong?) { self.isLoadMore = isLoadMore self.list = list self.unread = unread // Saving current visible cells - currentItems.removeAll(keepCapacity: true) - let visibleItems = self.collectionView!.indexPathsForVisibleItems() + currentItems.removeAll(keepingCapacity: true) + let visibleItems = self.collectionView!.indexPathsForVisibleItems let currentOffset = self.collectionView!.contentOffset.y for indexPath in visibleItems { - let index = indexPath.item + let index = (indexPath as NSIndexPath).item let topOffset = items[index]!.attrs.frame.origin.y - currentOffset let id = items[index]!.id currentItems.append(AACachedLayout(id: id, offset: topOffset)) @@ -49,15 +49,15 @@ class AAMessagesFlowLayout : UICollectionViewLayout { isScrolledToEnd = self.collectionView!.contentOffset.y < 8 } - override func prepareLayout() { - super.prepareLayout() + override func prepare() { + super.prepare() autoreleasepool { // Validate sections - let sectionsCount = self.collectionView!.numberOfSections() + let sectionsCount = self.collectionView!.numberOfSections if sectionsCount == 0 { - items.removeAll(keepCapacity: false) + items.removeAll(keepingCapacity: false) contentHeight = 0.0 return } @@ -72,8 +72,8 @@ class AAMessagesFlowLayout : UICollectionViewLayout { } // items.removeAll(keepCapacity: false) // frames.removeAll(keepCapacity: false) - items.removeAll(keepCapacity: true) - frames.removeAll(keepCapacity: true) + items.removeAll(keepingCapacity: true) + frames.removeAll(keepingCapacity: true) if list != nil { @@ -86,14 +86,14 @@ class AAMessagesFlowLayout : UICollectionViewLayout { if itemId == unread { height += AABubbleCell.newMessageSize } - let itemSize = CGSizeMake(self.collectionView!.bounds.width, height) + let itemSize = CGSize(width: self.collectionView!.bounds.width, height: height) - let frame = CGRect(origin: CGPointMake(0, contentHeight), size: itemSize) + let frame = CGRect(origin: CGPoint(x: 0, y: contentHeight), size: itemSize) var item = AALayoutItem(id: itemId) item.size = itemSize - let attrs = UICollectionViewLayoutAttributes(forCellWithIndexPath: NSIndexPath(forRow: i, inSection: 0)) + let attrs = UICollectionViewLayoutAttributes(forCellWith: IndexPath(row: i, section: 0)) attrs.frame = frame item.attrs = attrs @@ -115,33 +115,33 @@ class AAMessagesFlowLayout : UICollectionViewLayout { } } - override func collectionViewContentSize() -> CGSize { + override var collectionViewContentSize : CGSize { return CGSize(width: self.collectionView!.bounds.width, height: contentHeight) } - override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { var res = [UICollectionViewLayoutAttributes]() for i in 0.. UICollectionViewLayoutAttributes? { - return items[indexPath.item]!.attrs + override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + return items[(indexPath as NSIndexPath).item]!.attrs } - override func initialLayoutAttributesForAppearingItemAtIndexPath(itemIndexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { - let res = super.initialLayoutAttributesForAppearingItemAtIndexPath(itemIndexPath) + override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + let res = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath) if !isLoadMore && insertedIndexPaths.contains(itemIndexPath) { res?.alpha = 0 - res?.transform = CGAffineTransformTranslate(CGAffineTransformIdentity, 0, -44) + res?.transform = CGAffineTransform.identity.translatedBy(x: 0, y: -44) } else { res?.alpha = 1 } @@ -184,7 +184,7 @@ class AAMessagesFlowLayout : UICollectionViewLayout { } if delta != nil { - self.collectionView!.contentOffset = CGPointMake(0, self.collectionView!.contentOffset.y - delta) + self.collectionView!.contentOffset = CGPoint(x: 0, y: self.collectionView!.contentOffset.y - delta) } } @@ -215,7 +215,7 @@ struct AACachedLayout { } @objc enum AAMessageGravity: Int { - case Left - case Right - case Center + case left + case right + case center } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Layouting/BubbleLayouts.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Layouting/BubbleLayouts.swift index 176ea727d8..1419814c2c 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Layouting/BubbleLayouts.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Layouting/BubbleLayouts.swift @@ -21,9 +21,9 @@ public func ==(lhs: AACellSetting, rhs: AACellSetting) -> Bool { return lhs.showDate == rhs.showDate && lhs.clenchTop == rhs.clenchTop && lhs.clenchBottom == rhs.clenchBottom } -private let dateFormatter = NSDateFormatter().initDateFormatter() +private let dateFormatter = DateFormatter().initDateFormatter() -public class AACellLayout { +open class AACellLayout { let layouter: AABubbleLayouter let key: String @@ -39,11 +39,11 @@ public class AACellLayout { self.layouter = layouter } - public class func formatDate(date: Int64) -> String { - return dateFormatter.stringFromDate(NSDate(timeIntervalSince1970: NSTimeInterval(Double(date) / 1000.0))) + open class func formatDate(_ date: Int64) -> String { + return dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(Double(date) / 1000.0))) } - public class func pickApproriateSize(width: CGFloat, height: CGFloat) -> CGSize { + open class func pickApproriateSize(_ width: CGFloat, height: CGFloat) -> CGSize { let maxW: CGFloat let maxH: CGFloat if AADevice.isiPad { @@ -65,8 +65,8 @@ public class AACellLayout { } } -extension NSDateFormatter { - func initDateFormatter() -> NSDateFormatter { +extension DateFormatter { + func initDateFormatter() -> DateFormatter { dateFormat = "HH:mm" return self } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Layouting/Caches.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Layouting/Caches.swift index 13da981b2b..f593930853 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Layouting/Caches.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Layouting/Caches.swift @@ -5,17 +5,17 @@ import Foundation class AACache { - private var cache = AAHashMap() + fileprivate var cache = AAHashMap() - func pick(id: Int64) -> T? { + func pick(_ id: Int64) -> T? { return cache.getValueAtKey(id) } - func cache(id: Int64, value: T) { + func cache(_ id: Int64, value: T) { cache.setKey(id, withValue: value) } - func revoke(id: Int64) { + func revoke(_ id: Int64) { cache.setKey(id, withValue: nil) } @@ -33,7 +33,7 @@ struct AACachedSetting { self.cached = cached } - func isValid(prev: ACMessage?, next:ACMessage?) -> Bool { + func isValid(_ prev: ACMessage?, next:ACMessage?) -> Bool { if prev?.rid != prevId { return false } @@ -51,13 +51,13 @@ class AALayoutCache : AACache { } class AAFastThumbCache { - private var thumbs = AAHashMap() + fileprivate var thumbs = AAHashMap() - func pick(id: Int64) -> UIImage? { + func pick(_ id: Int64) -> UIImage? { return thumbs.getValueAtKey(id) } - func cache(id: Int64, image: UIImage) { + func cache(_ id: Int64, image: UIImage) { thumbs.setKey(id, withValue: image) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Suggestion/AAAutoCompleteCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Suggestion/AAAutoCompleteCell.swift index 4dd6010e5c..663f6db4bd 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Suggestion/AAAutoCompleteCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Suggestion/AAAutoCompleteCell.swift @@ -8,17 +8,17 @@ import TTTAttributedLabel class AAAutoCompleteCell: AATableViewCell { var avatarView = AAAvatarView() - var nickView = TTTAttributedLabel(frame: CGRectZero) - var nameView = TTTAttributedLabel(frame: CGRectZero) + var nickView = TTTAttributedLabel(frame: CGRect.zero) + var nameView = TTTAttributedLabel(frame: CGRect.zero) override init(style: UITableViewCellStyle, reuseIdentifier: String?) { - super.init(style: UITableViewCellStyle.Default, reuseIdentifier: reuseIdentifier) + super.init(style: UITableViewCellStyle.default, reuseIdentifier: reuseIdentifier) // avatarView.enableAnimation = false - nickView.font = UIFont.systemFontOfSize(14) + nickView.font = UIFont.systemFont(ofSize: 14) nickView.textColor = ActorSDK.sharedActor().style.cellTextColor - nameView.font = UIFont.systemFontOfSize(14) + nameView.font = UIFont.systemFont(ofSize: 14) nameView.textColor = ActorSDK.sharedActor().style.cellHintColor self.contentView.addSubview(avatarView) @@ -30,7 +30,7 @@ class AAAutoCompleteCell: AATableViewCell { fatalError("init(coder:) has not been implemented") } - func bindData(user: ACMentionFilterResult, highlightWord: String) { + func bindData(_ user: ACMentionFilterResult, highlightWord: String) { avatarView.bind(user.mentionString, id: Int(user.uid), avatar: user.avatar) @@ -47,21 +47,21 @@ class AAAutoCompleteCell: AATableViewCell { let nickAttrs = NSMutableAttributedString(string: nickText) let nameAttrs = NSMutableAttributedString(string: nameText) - nickAttrs.addAttribute(NSFontAttributeName, value: UIFont.systemFontOfSize(14), range: NSMakeRange(0, nickText.length)) + nickAttrs.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: 14), range: NSMakeRange(0, nickText.length)) nickAttrs.addAttribute(NSForegroundColorAttributeName, value: ActorSDK.sharedActor().style.cellTextColor, range: NSMakeRange(0, nickText.length)) - nameAttrs.addAttribute(NSFontAttributeName, value: UIFont.systemFontOfSize(14), range: NSMakeRange(0, nameText.length)) + nameAttrs.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: 14), range: NSMakeRange(0, nameText.length)) nameAttrs.addAttribute(NSForegroundColorAttributeName, value: ActorSDK.sharedActor().style.cellHintColor, range: NSMakeRange(0, nameText.length)) for i in 0.. Void in - UIView.animateWithDuration(0.4, delay: 0.0, usingSpringWithDamping: 0.7, - initialSpringVelocity: 0.6, options: .CurveEaseInOut, animations: { - self.sheetView.frame = CGRectMake(0, self.frame.height - self.sheetViewHeight, self.frame.width, self.sheetViewHeight) + UIView.animate(withDuration: 0.4, delay: 0.0, usingSpringWithDamping: 0.7, + initialSpringVelocity: 0.6, options: UIViewAnimationOptions(), animations: { + self.sheetView.frame = CGRect(x: 0, y: self.frame.height - self.sheetViewHeight, width: self.frame.width, height: self.sheetViewHeight) self.backgroundView.alpha = 1 }, completion: nil) } } - public func dismiss() { + open func dismiss() { var nextFrame = self.sheetView.frame nextFrame.origin.y = self.presentedInController.view.height if let navigation = presentedInController as? UINavigationController { - navigation.interactivePopGestureRecognizer?.enabled = true + navigation.interactivePopGestureRecognizer?.isEnabled = true } else if let navigation = presentedInController.navigationController { - navigation.interactivePopGestureRecognizer?.enabled = true + navigation.interactivePopGestureRecognizer?.isEnabled = true } - UIView.animateWithDuration(0.25, animations: { () -> Void in + UIView.animate(withDuration: 0.25, animations: { () -> Void in self.sheetView.frame = nextFrame - self.backgroundView.alpha = 0}) { (bool) -> Void in + self.backgroundView.alpha = 0}, completion: { (bool) -> Void in self.delegate = nil if self.thumbnailView != nil { self.thumbnailView.dismiss() self.thumbnailView = nil } self.removeFromSuperview() - } + }) } - private func setupAllViews() { + fileprivate func setupAllViews() { // @@ -123,26 +123,26 @@ public class AAConvActionSheet: UIView, AAThumbnailViewDelegate { if enablePhotoPicker { - self.thumbnailView = AAThumbnailView(frame: CGRectMake(0, 5, superWidth, 90)) + self.thumbnailView = AAThumbnailView(frame: CGRect(x: 0, y: 5, width: superWidth, height: 90)) self.thumbnailView.delegate = self self.thumbnailView.open() self.btnCamera = { - let button = UIButton(type: UIButtonType.System) + let button = UIButton(type: UIButtonType.system) button.tintColor = UIColor(red: 5.0/255.0, green: 124.0/255.0, blue: 226.0/255.0, alpha: 1) - button.titleLabel?.font = UIFont.systemFontOfSize(17) - button.setTitle(AALocalized("PhotoCamera"), forState: UIControlState.Normal) - button.addTarget(self, action: #selector(AAConvActionSheet.btnCameraAction), forControlEvents: UIControlEvents.TouchUpInside) + button.titleLabel?.font = UIFont.systemFont(ofSize: 17) + button.setTitle(AALocalized("PhotoCamera"), for: UIControlState()) + button.addTarget(self, action: #selector(AAConvActionSheet.btnCameraAction), for: UIControlEvents.touchUpInside) return button }() self.buttons.append(self.btnCamera) self.btnLibrary = { - let button = UIButton(type: UIButtonType.System) + let button = UIButton(type: UIButtonType.system) button.tintColor = UIColor(red: 5.0/255.0, green: 124.0/255.0, blue: 226.0/255.0, alpha: 1) - button.titleLabel?.font = UIFont.systemFontOfSize(17) - button.setTitle(AALocalized("PhotoLibrary"), forState: UIControlState.Normal) - button.addTarget(self, action: #selector(AAConvActionSheet.btnLibraryAction), forControlEvents: UIControlEvents.TouchUpInside) + button.titleLabel?.font = UIFont.systemFont(ofSize: 17) + button.setTitle(AALocalized("PhotoLibrary"), for: UIControlState()) + button.addTarget(self, action: #selector(AAConvActionSheet.btnLibraryAction), for: UIControlEvents.touchUpInside) return button }() self.buttons.append(self.btnLibrary) @@ -153,22 +153,22 @@ public class AAConvActionSheet: UIView, AAThumbnailViewDelegate { for i in 0.. 0 { var sendString:String @@ -226,14 +226,14 @@ public class AAConvActionSheet: UIView, AAThumbnailViewDelegate { // // remove target // - self.btnCamera.removeTarget(self, action: #selector(AAConvActionSheet.btnCameraAction), forControlEvents: UIControlEvents.TouchUpInside) + self.btnCamera.removeTarget(self, action: #selector(AAConvActionSheet.btnCameraAction), for: UIControlEvents.touchUpInside) // // add new target // - self.btnCamera.setTitle(sendString, forState: UIControlState.Normal) - self.btnCamera.addTarget(self, action:#selector(AAConvActionSheet.sendPhotos), forControlEvents: UIControlEvents.TouchUpInside) + self.btnCamera.setTitle(sendString, for: UIControlState()) + self.btnCamera.addTarget(self, action:#selector(AAConvActionSheet.sendPhotos), for: UIControlEvents.touchUpInside) self.btnCamera.titleLabel?.font = UIFont(name: "HelveticaNeue-Medium", size: 17) @@ -242,14 +242,14 @@ public class AAConvActionSheet: UIView, AAThumbnailViewDelegate { // // remove target // - self.btnCamera.removeTarget(self, action: #selector(AAConvActionSheet.sendPhotos), forControlEvents: UIControlEvents.TouchUpInside) + self.btnCamera.removeTarget(self, action: #selector(AAConvActionSheet.sendPhotos), for: UIControlEvents.touchUpInside) // // add new target // - self.btnCamera.setTitle(AALocalized("PhotoCamera"), forState: UIControlState.Normal) - self.btnCamera.addTarget(self, action: #selector(AAConvActionSheet.btnCameraAction), forControlEvents: UIControlEvents.TouchUpInside) - self.btnCamera.titleLabel?.font = UIFont.systemFontOfSize(17) + self.btnCamera.setTitle(AALocalized("PhotoCamera"), for: UIControlState()) + self.btnCamera.addTarget(self, action: #selector(AAConvActionSheet.btnCameraAction), for: UIControlEvents.touchUpInside) + self.btnCamera.titleLabel?.font = UIFont.systemFont(ofSize: 17) } } @@ -260,8 +260,8 @@ public class AAConvActionSheet: UIView, AAThumbnailViewDelegate { func sendPhotos() { if self.thumbnailView != nil { - self.thumbnailView.getSelectedAsImages { (images:[(NSData,Bool)]) -> () in - self.delegate?.actionSheetPickedImages(images) + self.thumbnailView.getSelectedAsImages { (images:[(Data,Bool)]) -> () in + (self.delegate?.actionSheetPickedImages(images))! } } dismiss() @@ -277,7 +277,7 @@ public class AAConvActionSheet: UIView, AAThumbnailViewDelegate { dismiss() } - func btnCustomAction(sender: UIButton) { + func btnCustomAction(_ sender: UIButton) { delegate?.actionSheetCustomButton(sender.tag) dismiss() } @@ -286,7 +286,7 @@ public class AAConvActionSheet: UIView, AAThumbnailViewDelegate { dismiss() } - public override func touchesBegan(touches: Set, withEvent event: UIEvent?) { + open override func touchesBegan(_ touches: Set, with event: UIEvent?) { dismiss() } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AARecordAudioController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AARecordAudioController.swift index 874813a674..f048235081 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AARecordAudioController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AARecordAudioController.swift @@ -23,15 +23,15 @@ class AARecordAudioController: UIViewController,UIViewControllerTransitioningDel // var filePath : String! - var fileDuration : NSTimeInterval! + var fileDuration : TimeInterval! var recorded : Bool! = false - private let audioRecorder: AAAudioRecorder! = AAAudioRecorder() - private var audioPlayer: AAModernConversationAudioPlayer! + fileprivate let audioRecorder: AAAudioRecorder! = AAAudioRecorder() + fileprivate var audioPlayer: AAModernConversationAudioPlayer! - var meterTimer:NSTimer! - var soundFileURL:NSURL? + var meterTimer:Timer! + var soundFileURL:URL? //////////////////////////////// @@ -42,7 +42,7 @@ class AARecordAudioController: UIViewController,UIViewControllerTransitioningDel } - override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) { + override init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: Bundle!) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) self.commonInit() @@ -50,27 +50,27 @@ class AARecordAudioController: UIViewController,UIViewControllerTransitioningDel } func commonInit() { - self.modalPresentationStyle = .Custom + self.modalPresentationStyle = .custom self.transitioningDelegate = self } - override func preferredStatusBarStyle() -> UIStatusBarStyle { - return UIStatusBarStyle.LightContent + override var preferredStatusBarStyle : UIStatusBarStyle { + return UIStatusBarStyle.lightContent } // ---- UIViewControllerTransitioningDelegate methods - func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController, sourceViewController source: UIViewController) -> UIPresentationController? { + func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { if presented == self { - return AACustomPresentationController(presentedViewController: presented, presentingViewController: presenting) + return AACustomPresentationController(presentedViewController: presented, presenting: presenting) } return nil } - func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { if presented == self { return AACustomPresentationAnimationController(isPresenting: true) @@ -80,7 +80,7 @@ class AARecordAudioController: UIViewController,UIViewControllerTransitioningDel } } - func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { if dismissed == self { return AACustomPresentationAnimationController(isPresenting: false) @@ -94,77 +94,77 @@ class AARecordAudioController: UIViewController,UIViewControllerTransitioningDel super.loadView() self.recorderView = UIView() - self.recorderView.frame = CGRectMake(self.view.frame.width/2 - 120, self.view.frame.height/2 - 80, 240, 160) - self.recorderView.backgroundColor = UIColor.whiteColor() + self.recorderView.frame = CGRect(x: self.view.frame.width/2 - 120, y: self.view.frame.height/2 - 80, width: 240, height: 160) + self.recorderView.backgroundColor = UIColor.white self.recorderView.layer.cornerRadius = 10 self.recorderView.layer.masksToBounds = true self.view.addSubview(self.recorderView) - self.buttonClose = UIButton(type: UIButtonType.System) - self.buttonClose.addTarget(self, action: #selector(AARecordAudioController.closeController), forControlEvents: UIControlEvents.TouchUpInside) - self.buttonClose.tintColor = UIColor.whiteColor() - self.buttonClose.setImage(UIImage.bundled("aa_closerecordbutton"), forState: UIControlState.Normal) - self.buttonClose.frame = CGRectMake(205, 5, 25, 25) + self.buttonClose = UIButton(type: UIButtonType.system) + self.buttonClose.addTarget(self, action: #selector(AARecordAudioController.closeController), for: UIControlEvents.touchUpInside) + self.buttonClose.tintColor = UIColor.white + self.buttonClose.setImage(UIImage.bundled("aa_closerecordbutton"), for: UIControlState()) + self.buttonClose.frame = CGRect(x: 205, y: 5, width: 25, height: 25) self.recorderView.addSubview(self.buttonClose) let separatorView = UIView() - separatorView.frame = CGRectMake(0, 80, 240, 0.5) - separatorView.backgroundColor = UIColor.grayColor() + separatorView.frame = CGRect(x: 0, y: 80, width: 240, height: 0.5) + separatorView.backgroundColor = UIColor.gray self.recorderView.addSubview(separatorView) self.timerLabel = UILabel() self.timerLabel.text = "00:00" self.timerLabel.font = UIFont(name: "HelveticaNeue-Medium", size: 17)! self.timerLabel.textColor = ActorSDK.sharedActor().style.vcHintColor - self.timerLabel.frame = CGRectMake(70, 5, 100, 40) - self.timerLabel.textAlignment = .Center - self.timerLabel.backgroundColor = UIColor.clearColor() + self.timerLabel.frame = CGRect(x: 70, y: 5, width: 100, height: 40) + self.timerLabel.textAlignment = .center + self.timerLabel.backgroundColor = UIColor.clear self.recorderView.addSubview(self.timerLabel) - self.startRecButton = UIButton(type: UIButtonType.System) - self.startRecButton.tintColor = UIColor.redColor() - self.startRecButton.setImage(UIImage.bundled("aa_startrecordbutton"), forState: UIControlState.Normal) - self.startRecButton.addTarget(self, action: #selector(AARecordAudioController.startRec), forControlEvents: UIControlEvents.TouchUpInside) - self.startRecButton.frame = CGRectMake(100, 110, 40, 40) + self.startRecButton = UIButton(type: UIButtonType.system) + self.startRecButton.tintColor = UIColor.red + self.startRecButton.setImage(UIImage.bundled("aa_startrecordbutton"), for: UIControlState()) + self.startRecButton.addTarget(self, action: #selector(AARecordAudioController.startRec), for: UIControlEvents.touchUpInside) + self.startRecButton.frame = CGRect(x: 100, y: 110, width: 40, height: 40) self.recorderView.addSubview(self.startRecButton) - self.stopRecButton = UIButton(type: UIButtonType.System) - self.stopRecButton.tintColor = UIColor.redColor() - self.stopRecButton.setImage(UIImage.bundled("aa_pauserecordbutton"), forState: UIControlState.Normal) - self.stopRecButton.addTarget(self, action: #selector(AARecordAudioController.stopRec), forControlEvents: UIControlEvents.TouchUpInside) - self.stopRecButton.frame = CGRectMake(100, 110, 40, 40) + self.stopRecButton = UIButton(type: UIButtonType.system) + self.stopRecButton.tintColor = UIColor.red + self.stopRecButton.setImage(UIImage.bundled("aa_pauserecordbutton"), for: UIControlState()) + self.stopRecButton.addTarget(self, action: #selector(AARecordAudioController.stopRec), for: UIControlEvents.touchUpInside) + self.stopRecButton.frame = CGRect(x: 100, y: 110, width: 40, height: 40) self.recorderView.addSubview(self.stopRecButton) - self.stopRecButton.hidden = true + self.stopRecButton.isHidden = true - self.playRecButton = UIButton(type: UIButtonType.System) - self.playRecButton.tintColor = UIColor.greenColor() - self.playRecButton.setImage(UIImage.bundled("aa_playrecordbutton"), forState: UIControlState.Normal) - self.playRecButton.addTarget(self, action: #selector(AARecordAudioController.play), forControlEvents: UIControlEvents.TouchUpInside) - self.playRecButton.frame = CGRectMake(100, 110, 40, 40) + self.playRecButton = UIButton(type: UIButtonType.system) + self.playRecButton.tintColor = UIColor.green + self.playRecButton.setImage(UIImage.bundled("aa_playrecordbutton"), for: UIControlState()) + self.playRecButton.addTarget(self, action: #selector(AARecordAudioController.play), for: UIControlEvents.touchUpInside) + self.playRecButton.frame = CGRect(x: 100, y: 110, width: 40, height: 40) self.recorderView.addSubview(self.playRecButton) - self.playRecButton.hidden = true + self.playRecButton.isHidden = true - self.sendRecord = UIButton(type: UIButtonType.System) - self.sendRecord.tintColor = UIColor.greenColor() - self.sendRecord.setImage(UIImage.bundled("aa_sendrecord"), forState: UIControlState.Normal) - self.sendRecord.addTarget(self, action: #selector(AARecordAudioController.sendRecordMessage), forControlEvents: UIControlEvents.TouchUpInside) - self.sendRecord.frame = CGRectMake(190, 115, 40, 40) - self.sendRecord.enabled = false + self.sendRecord = UIButton(type: UIButtonType.system) + self.sendRecord.tintColor = UIColor.green + self.sendRecord.setImage(UIImage.bundled("aa_sendrecord"), for: UIControlState()) + self.sendRecord.addTarget(self, action: #selector(AARecordAudioController.sendRecordMessage), for: UIControlEvents.touchUpInside) + self.sendRecord.frame = CGRect(x: 190, y: 115, width: 40, height: 40) + self.sendRecord.isEnabled = false self.recorderView.addSubview(self.sendRecord) - self.cleanRecord = UIButton(type: UIButtonType.System) - self.cleanRecord.tintColor = UIColor.redColor() - self.cleanRecord.setImage(UIImage.bundled("aa_deleterecord"), forState: UIControlState.Normal) - self.cleanRecord.addTarget(self, action: #selector(AARecordAudioController.sendRecordMessage), forControlEvents: UIControlEvents.TouchUpInside) - self.cleanRecord.frame = CGRectMake(10, 120, 30, 30) - self.cleanRecord.enabled = false + self.cleanRecord = UIButton(type: UIButtonType.system) + self.cleanRecord.tintColor = UIColor.red + self.cleanRecord.setImage(UIImage.bundled("aa_deleterecord"), for: UIControlState()) + self.cleanRecord.addTarget(self, action: #selector(AARecordAudioController.sendRecordMessage), for: UIControlEvents.touchUpInside) + self.cleanRecord.frame = CGRect(x: 10, y: 120, width: 30, height: 30) + self.cleanRecord.isEnabled = false self.recorderView.addSubview(self.cleanRecord) @@ -184,7 +184,7 @@ class AARecordAudioController: UIViewController,UIViewControllerTransitioningDel func closeController() { self.audioRecorder.cancel() - self.dismissViewControllerAnimated(true, completion: nil) + self.dismiss(animated: true, completion: nil) } @@ -193,9 +193,9 @@ class AARecordAudioController: UIViewController,UIViewControllerTransitioningDel //log.debug("recording. recorder nil") - playRecButton.hidden = true - stopRecButton.hidden = false - startRecButton.hidden = true + playRecButton.isHidden = true + stopRecButton.isHidden = false + startRecButton.isHidden = true startTimer() @@ -209,22 +209,22 @@ class AARecordAudioController: UIViewController,UIViewControllerTransitioningDel meterTimer.invalidate() - playRecButton.hidden = false - startRecButton.hidden = true - stopRecButton.hidden = true + playRecButton.isHidden = false + startRecButton.isHidden = true + stopRecButton.isHidden = true - audioRecorder.finish({ (path: String!, duration: NSTimeInterval) -> Void in + audioRecorder.finish({ (path: String?, duration: TimeInterval) -> Void in if (nil == path) { print("onAudioRecordingFinished: empty path") return } - self.filePath = path + self.filePath = path! self.fileDuration = duration - dispatch_async(dispatch_get_main_queue(), { () -> Void in - self.sendRecord.enabled = true - self.cleanRecord.enabled = true + DispatchQueue.main.async(execute: { () -> Void in + self.sendRecord.isEnabled = true + self.cleanRecord.isEnabled = true }) }) @@ -245,9 +245,9 @@ class AARecordAudioController: UIViewController,UIViewControllerTransitioningDel self.audioPlayer = AAModernConversationAudioPlayer(filePath:self.filePath) self.audioPlayer.play(0) - playRecButton.hidden = true - startRecButton.hidden = true - stopRecButton.hidden = false + playRecButton.isHidden = true + startRecButton.isHidden = true + stopRecButton.isHidden = false } @@ -255,12 +255,12 @@ class AARecordAudioController: UIViewController,UIViewControllerTransitioningDel func recordWithPermission() { let session:AVAudioSession = AVAudioSession.sharedInstance() // ios 8 and later - if (session.respondsToSelector(#selector(AVAudioSession.requestRecordPermission(_:)))) { + if (session.responds(to: #selector(AVAudioSession.requestRecordPermission(_:)))) { AVAudioSession.sharedInstance().requestRecordPermission({(granted: Bool)-> Void in if granted { print("Permission to record granted") self.setSessionPlayAndRecord() - self.meterTimer = NSTimer.scheduledTimerWithTimeInterval(0.1, + self.meterTimer = Timer.scheduledTimer(timeInterval: 0.1, target:self, selector:#selector(AARecordAudioController.updateAudioMeter(_:)), userInfo:nil, @@ -280,21 +280,21 @@ class AARecordAudioController: UIViewController,UIViewControllerTransitioningDel } func startTimer() { - self.meterTimer = NSTimer.scheduledTimerWithTimeInterval(0.1, + self.meterTimer = Timer.scheduledTimer(timeInterval: 0.1, target:self, selector:#selector(AARecordAudioController.updateAudioMeter(_:)), userInfo:nil, repeats:true) } - func updateAudioMeter(timer:NSTimer) { + func updateAudioMeter(_ timer:Timer) { if (self.audioRecorder != nil) { let dur = self.audioRecorder.currentDuration() let min = Int(dur / 60) - let sec = Int(dur % 60) + let sec = Int(dur.truncatingRemainder(dividingBy: 60)) let s = String(format: "%02d:%02d", min, sec) self.timerLabel.text = s @@ -336,7 +336,7 @@ class AARecordAudioController: UIViewController,UIViewControllerTransitioningDel func sendRecordMessage() { - self.dismissViewControllerAnimated(true, completion: nil) + self.dismiss(animated: true, completion: nil) //self.chatController.sendVoiceMessage(self.filePath, duration: self.fileDuration) } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AAStickersKeyboard.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AAStickersKeyboard.swift index 3d7da9fbad..c6d2b25e70 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AAStickersKeyboard.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AAStickersKeyboard.swift @@ -6,16 +6,16 @@ import UIKit import YYImage public protocol AAStickersKeyboardDelegate { - func stickerDidSelected(keyboard: AAStickersKeyboard, sticker: ACSticker) + func stickerDidSelected(_ keyboard: AAStickersKeyboard, sticker: ACSticker) } -public class AAStickersKeyboard: UIView, UICollectionViewDelegate, UICollectionViewDataSource { +open class AAStickersKeyboard: UIView, UICollectionViewDelegate, UICollectionViewDataSource { - public var delegate : AAStickersKeyboardDelegate? + open var delegate : AAStickersKeyboardDelegate? - private let collectionView: UICollectionView! - private var stickers = Array() - private let binder = AABinder() + fileprivate let collectionView: UICollectionView! + fileprivate var stickers = Array() + fileprivate let binder = AABinder() public override init(frame: CGRect) { @@ -24,8 +24,8 @@ public class AAStickersKeyboard: UIView, UICollectionViewDelegate, UICollectionV // layout for collection view let layoutCV = UICollectionViewFlowLayout() - layoutCV.scrollDirection = .Vertical - layoutCV.itemSize = CGSizeMake(widthHightItem, widthHightItem) + layoutCV.scrollDirection = .vertical + layoutCV.itemSize = CGSize(width: widthHightItem, height: widthHightItem) layoutCV.sectionInset = UIEdgeInsets(top: 5, left: 0, bottom: 10, right: 0) // init collection view @@ -41,27 +41,27 @@ public class AAStickersKeyboard: UIView, UICollectionViewDelegate, UICollectionV self.collectionView.dataSource = self self.collectionView.backgroundColor = UIColor(red: 0.7728, green: 0.8874, blue: 0.9365, alpha: 1.0) - self.collectionView.registerClass(AAStickersViewCell.self, forCellWithReuseIdentifier: "AAStickersViewCell") + self.collectionView.register(AAStickersViewCell.self, forCellWithReuseIdentifier: "AAStickersViewCell") self.collectionView.contentInset = UIEdgeInsetsMake(10, 5, 10, 5) self.collectionView.preservesSuperviewLayoutMargins = false - self.collectionView.layoutMargins = UIEdgeInsetsZero + self.collectionView.layoutMargins = UIEdgeInsets.zero // add collection view as subview self.addSubview(self.collectionView) - self.backgroundColor = UIColor.clearColor() + self.backgroundColor = UIColor.clear // Bind To Stickers - binder.bind(Actor.getAvailableStickersVM().getOwnStickerPacks()) { (value: JavaUtilArrayList!) -> () in - self.stickers.removeAll(keepCapacity: true) + binder.bind(Actor.getAvailableStickersVM().getOwnStickerPacks()) { (value: JavaUtilArrayList?) -> () in + self.stickers.removeAll(keepingCapacity: true) - for i in 0.. Int { + open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return stickers.count } - public func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { + open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let stickerCell = collectionView - .dequeueReusableCellWithReuseIdentifier("AAStickersViewCell", forIndexPath: indexPath) as! AAStickersViewCell - stickerCell.bind(stickers[indexPath.row], clearPrev: true) + .dequeueReusableCell(withReuseIdentifier: "AAStickersViewCell", for: indexPath) as! AAStickersViewCell + stickerCell.bind(stickers[(indexPath as NSIndexPath).row], clearPrev: true) return stickerCell } - public func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { - let sticker = stickers[indexPath.row] + open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let sticker = stickers[(indexPath as NSIndexPath).row] self.delegate?.stickerDidSelected(self, sticker: sticker) } } -public class AAStickersViewCell : UICollectionViewCell { +open class AAStickersViewCell : UICollectionViewCell { - private let stickerImage = AAStickerView() + fileprivate let stickerImage = AAStickerView() public override init(frame: CGRect) { super.init(frame: frame) - self.backgroundColor = UIColor.clearColor() - self.contentView.backgroundColor = UIColor.clearColor() + self.backgroundColor = UIColor.clear + self.contentView.backgroundColor = UIColor.clear self.addSubview(self.stickerImage) } @@ -120,7 +120,7 @@ public class AAStickersViewCell : UICollectionViewCell { fatalError("init(coder:) has not been implemented") } - public func bind(sticker: ACSticker!, clearPrev: Bool) { + open func bind(_ sticker: ACSticker!, clearPrev: Bool) { var fileLocation: ACFileReference? = sticker.getImage128() if sticker.getImage256() != nil { fileLocation = sticker.getImage256() @@ -128,12 +128,12 @@ public class AAStickersViewCell : UICollectionViewCell { self.stickerImage.setSticker(fileLocation) } - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() self.stickerImage.frame = self.contentView.frame } - public override func prepareForReuse() { + open override func prepareForReuse() { super.prepareForReuse() self.stickerImage.setSticker(nil) } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AAThumbnailCollectionCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AAThumbnailCollectionCell.swift index 0f42b8efc1..c280582f0a 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AAThumbnailCollectionCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AAThumbnailCollectionCell.swift @@ -10,7 +10,7 @@ class AAThumbnailCollectionCell: UICollectionViewCell { let imgSelected : UIImageView! var animated : Bool! - var indexPath : NSIndexPath! + var indexPath : IndexPath! var isCheckSelected : Bool! weak var bindedThumbView : AAThumbnailView! weak var bindedPhotoModel : PHAsset! @@ -18,12 +18,12 @@ class AAThumbnailCollectionCell: UICollectionViewCell { override init(frame: CGRect) { self.imgThumbnails = UIImageView() - self.imgThumbnails.backgroundColor = UIColor.clearColor() + self.imgThumbnails.backgroundColor = UIColor.clear self.imgSelected = UIImageView() - self.imgSelected.backgroundColor = UIColor.clearColor() - self.imgSelected.userInteractionEnabled = true - self.imgSelected.contentMode = UIViewContentMode.ScaleAspectFill + self.imgSelected.backgroundColor = UIColor.clear + self.imgSelected.isUserInteractionEnabled = true + self.imgSelected.contentMode = UIViewContentMode.scaleAspectFill self.isCheckSelected = false self.animated = false @@ -49,10 +49,10 @@ class AAThumbnailCollectionCell: UICollectionViewCell { self.imgSelected.image = UIImage.bundled("ImageSelectedOn") - self.imgSelected.transform = CGAffineTransformMakeScale(0.5,0.5) - UIView.animateWithDuration(0.3, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in + self.imgSelected.transform = CGAffineTransform(scaleX: 0.5,y: 0.5) + UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: UIViewAnimationOptions(), animations: { () -> Void in - self.imgSelected.transform = CGAffineTransformIdentity + self.imgSelected.transform = CGAffineTransform.identity }, completion: nil) @@ -64,10 +64,10 @@ class AAThumbnailCollectionCell: UICollectionViewCell { self.imgSelected.image = UIImage.bundled("ImageSelectedOff") - self.imgSelected.transform = CGAffineTransformMakeScale(0.5,0.5) - UIView.animateWithDuration(0.3, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in + self.imgSelected.transform = CGAffineTransform(scaleX: 0.5,y: 0.5) + UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: UIViewAnimationOptions(), animations: { () -> Void in - self.imgSelected.transform = CGAffineTransformIdentity + self.imgSelected.transform = CGAffineTransform.identity }, completion: nil) @@ -89,9 +89,9 @@ class AAThumbnailCollectionCell: UICollectionViewCell { override func layoutSubviews() { super.layoutSubviews() - self.imgThumbnails.frame = CGRectMake(0, 0, 90, 90) + self.imgThumbnails.frame = CGRect(x: 0, y: 0, width: 90, height: 90) - self.imgSelected.frame = CGRectMake(self.contentView.frame.size.width-30, 4, 26, 26) + self.imgSelected.frame = CGRect(x: self.contentView.frame.size.width-30, y: 4, width: 26, height: 26) diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AAThumbnailView.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AAThumbnailView.swift index acfb072a7b..66c517e5ca 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AAThumbnailView.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AAThumbnailView.swift @@ -6,35 +6,35 @@ import UIKit import Photos public enum ImagePickerMediaType { - case Image - case Video - case ImageAndVideo + case image + case video + case imageAndVideo } public protocol AAThumbnailViewDelegate { - func thumbnailSelectedUpdated(selectedAssets: [(PHAsset,Bool)]) + func thumbnailSelectedUpdated(_ selectedAssets: [(PHAsset,Bool)]) } -public class AAThumbnailView: UIView,UICollectionViewDelegate , UICollectionViewDataSource { +open class AAThumbnailView: UIView,UICollectionViewDelegate , UICollectionViewDataSource { - public var delegate : AAThumbnailViewDelegate? + open var delegate : AAThumbnailViewDelegate? - private var collectionView:UICollectionView! - private let mediaType: ImagePickerMediaType = ImagePickerMediaType.Image + fileprivate var collectionView:UICollectionView! + fileprivate let mediaType: ImagePickerMediaType = ImagePickerMediaType.image - private var assets = [(PHAsset,Bool)]() - private var selectedAssets = [(PHAsset, Bool)]() - private var imageManager : PHCachingImageManager! + fileprivate var assets = [(PHAsset,Bool)]() + fileprivate var selectedAssets = [(PHAsset, Bool)]() + fileprivate var imageManager : PHCachingImageManager! - private let minimumPreviewHeight: CGFloat = 90 - private var maximumPreviewHeight: CGFloat = 90 + fileprivate let minimumPreviewHeight: CGFloat = 90 + fileprivate var maximumPreviewHeight: CGFloat = 90 - private let previewCollectionViewInset: CGFloat = 5 + fileprivate let previewCollectionViewInset: CGFloat = 5 - private lazy var requestOptions: PHImageRequestOptions = { + fileprivate lazy var requestOptions: PHImageRequestOptions = { let options = PHImageRequestOptions() - options.deliveryMode = .HighQualityFormat - options.resizeMode = .Fast + options.deliveryMode = .highQualityFormat + options.resizeMode = .fast return options }() @@ -52,22 +52,22 @@ public class AAThumbnailView: UIView,UICollectionViewDelegate , UICollectionView /// - public func open() { + open func open() { dispatchBackground { () -> Void in - if PHPhotoLibrary.authorizationStatus() == .Authorized { + if PHPhotoLibrary.authorizationStatus() == .authorized { self.imageManager = PHCachingImageManager() self.fetchAssets() - dispatch_async(dispatch_get_main_queue()) { + DispatchQueue.main.async { self.collectionView.reloadData() } - } else if PHPhotoLibrary.authorizationStatus() == .NotDetermined { + } else if PHPhotoLibrary.authorizationStatus() == .notDetermined { PHPhotoLibrary.requestAuthorization() { status in - if status == .Authorized { - dispatch_async(dispatch_get_main_queue()) { + if status == .authorized { + DispatchQueue.main.async { self.imageManager = PHCachingImageManager() self.fetchAssets() self.collectionView.reloadData() @@ -81,7 +81,7 @@ public class AAThumbnailView: UIView,UICollectionViewDelegate , UICollectionView } - private func fetchAssets() { + fileprivate func fetchAssets() { self.assets = [(PHAsset,Bool)]() let options = PHFetchOptions() @@ -96,71 +96,71 @@ public class AAThumbnailView: UIView,UICollectionViewDelegate , UICollectionView // options.predicate = NSPredicate(format: "mediaType = %d OR mediaType = %d", PHAssetMediaType.Image.rawValue, PHAssetMediaType.Video.rawValue) // } - options.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.Image.rawValue) + options.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.image.rawValue) let fetchLimit = 100 if #available(iOS 9, *) { options.fetchLimit = fetchLimit } - let result = PHAsset.fetchAssetsWithOptions(options) + let result = PHAsset.fetchAssets(with: options) let requestOptions = PHImageRequestOptions() - requestOptions.synchronous = true - requestOptions.deliveryMode = .FastFormat + requestOptions.isSynchronous = true + requestOptions.deliveryMode = .fastFormat - result.enumerateObjectsUsingBlock { asset, _, stop in - - if self.assets.count > fetchLimit { - stop.initialize(true) - } - - if let asset = asset as? PHAsset { - var isGIF = false - self.imageManager.requestImageDataForAsset(asset, options: requestOptions) { data, _, _, info in - if data != nil { - let gifMarker = info!["PHImageFileURLKey"] as! NSURL - print(gifMarker.pathExtension) - isGIF = (gifMarker.pathExtension == "GIF") ? true : false - print(isGIF) - self.prefetchImagesForAsset(asset) - } - self.assets.append((asset,isGIF)) - } - } - } +// result.enumerateObjects { asset, _, stop in +// +// if self.assets.count > fetchLimit { +// stop.initialize(to: true) +// } +// +// if let asset = asset as? PHAsset { +// var isGIF = false +// self.imageManager.requestImageData(for: asset, options: requestOptions) { data, _, _, info in +// if data != nil { +// let gifMarker = info!["PHImageFileURLKey"] as! URL +// print(gifMarker.pathExtension) +// isGIF = (gifMarker.pathExtension == "GIF") ? true : false +// print(isGIF) +// self.prefetchImagesForAsset(asset) +// } +// self.assets.append((asset,isGIF)) +// } +// } +// } } - private func prefetchImagesForAsset(asset: PHAsset) { - let targetSize = sizeForAsset(asset, scale: UIScreen.mainScreen().scale) - imageManager.startCachingImagesForAssets([asset], targetSize: targetSize, contentMode: .AspectFill, options: requestOptions) + fileprivate func prefetchImagesForAsset(_ asset: PHAsset) { + let targetSize = sizeForAsset(asset, scale: UIScreen.main.scale) + imageManager.startCachingImages(for: [asset], targetSize: targetSize, contentMode: .aspectFill, options: requestOptions) } - private func requestImageForAsset(asset: PHAsset, completion: (image: UIImage?) -> ()) { - let targetSize = sizeForAsset(asset, scale: UIScreen.mainScreen().scale) - requestOptions.synchronous = false + fileprivate func requestImageForAsset(_ asset: PHAsset, completion: @escaping (_ image: UIImage?) -> ()) { + let targetSize = sizeForAsset(asset, scale: UIScreen.main.scale) + requestOptions.isSynchronous = false // Workaround because PHImageManager.requestImageForAsset doesn't work for burst images if asset.representsBurst { - imageManager.requestImageDataForAsset(asset, options: requestOptions) { data, _, _, _ in + imageManager.requestImageData(for: asset, options: requestOptions) { data, _, _, _ in let image = data.flatMap { UIImage(data: $0) } - completion(image: image) + completion(image) } } else { - imageManager.requestImageForAsset(asset, targetSize: targetSize, contentMode: .AspectFill, options: requestOptions) { image, _ in - completion(image: image) + imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: requestOptions) { image, _ in + completion(image) } } } - private func sizeForAsset(asset: PHAsset, scale: CGFloat = 1) -> CGSize { + fileprivate func sizeForAsset(_ asset: PHAsset, scale: CGFloat = 1) -> CGSize { var complitedCGSize : CGSize! if asset.pixelWidth > asset.pixelHeight { - complitedCGSize = CGSizeMake(CGFloat(asset.pixelHeight),CGFloat(asset.pixelHeight)) + complitedCGSize = CGSize(width: CGFloat(asset.pixelHeight),height: CGFloat(asset.pixelHeight)) } else { - complitedCGSize = CGSizeMake(CGFloat(asset.pixelWidth),CGFloat(asset.pixelWidth)) + complitedCGSize = CGSize(width: CGFloat(asset.pixelWidth),height: CGFloat(asset.pixelWidth)) } return complitedCGSize @@ -168,22 +168,22 @@ public class AAThumbnailView: UIView,UICollectionViewDelegate , UICollectionView /// collection view delegate - public func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { + open func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } - public func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return self.assets.count } - public func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { + open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = self.collectionView.dequeueReusableCellWithReuseIdentifier("AAThumbnailCollectionCell", forIndexPath: indexPath) as! AAThumbnailCollectionCell + let cell = self.collectionView.dequeueReusableCell(withReuseIdentifier: "AAThumbnailCollectionCell", for: indexPath) as! AAThumbnailCollectionCell cell.bindedThumbView = self - let photoModel = self.assets[indexPath.row].0 - let animated = self.assets[indexPath.row].1 + let photoModel = self.assets[(indexPath as NSIndexPath).row].0 + let animated = self.assets[(indexPath as NSIndexPath).row].1 cell.bindedPhotoModel = photoModel @@ -196,18 +196,18 @@ public class AAThumbnailView: UIView,UICollectionViewDelegate , UICollectionView cell.imgSelected.image = UIImage.bundled("ImageSelectedOff") } - cell.backgroundColor = UIColor.whiteColor() + cell.backgroundColor = UIColor.white - let asset = assets[indexPath.row].0 + let asset = assets[(indexPath as NSIndexPath).row].0 requestImageForAsset(asset) { image in var complitedImage : UIImage! if image!.size.width > image!.size.height { - complitedImage = self.imageByCroppingImage(image!, toSize: CGSizeMake(image!.size.height,image!.size.height)) + complitedImage = self.imageByCroppingImage(image!, toSize: CGSize(width: image!.size.height,height: image!.size.height)) } else { - complitedImage = self.imageByCroppingImage(image!, toSize: CGSizeMake(image!.size.width,image!.size.width)) + complitedImage = self.imageByCroppingImage(image!, toSize: CGSize(width: image!.size.width,height: image!.size.width)) } cell.imgThumbnails.image = complitedImage @@ -221,79 +221,79 @@ public class AAThumbnailView: UIView,UICollectionViewDelegate , UICollectionView /// - public func reloadView() { + open func reloadView() { self.collectionView.reloadData() } - public func collectionViewSetup() { + open func collectionViewSetup() { let flowLayout = UICollectionViewFlowLayout() - flowLayout.scrollDirection = .Horizontal + flowLayout.scrollDirection = .horizontal flowLayout.minimumLineSpacing = 4 flowLayout.sectionInset = UIEdgeInsetsMake(5.0, 4.0, 5.0, 4.0) - flowLayout.itemSize = CGSizeMake(90, 90) + flowLayout.itemSize = CGSize(width: 90, height: 90) self.collectionView = UICollectionView(frame: self.bounds, collectionViewLayout: flowLayout) - self.collectionView.backgroundColor = UIColor.clearColor() + self.collectionView.backgroundColor = UIColor.clear self.collectionView.showsHorizontalScrollIndicator = false self.collectionView.delegate = self self.collectionView.dataSource = self - self.collectionView.registerClass(AAThumbnailCollectionCell.self, forCellWithReuseIdentifier: "AAThumbnailCollectionCell") + self.collectionView.register(AAThumbnailCollectionCell.self, forCellWithReuseIdentifier: "AAThumbnailCollectionCell") self.addSubview(self.collectionView) } - public func imageByCroppingImage(image:UIImage,toSize:CGSize) -> UIImage { + open func imageByCroppingImage(_ image:UIImage,toSize:CGSize) -> UIImage { - let refWidth = CGImageGetWidth(image.CGImage) - let refHeight = CGImageGetHeight(image.CGImage) + let refWidth = image.cgImage?.width + let refHeight = image.cgImage?.height - let x = CGFloat((refWidth - Int(toSize.width)) / 2) - let y = CGFloat((refHeight - Int(toSize.height)) / 2) + let x = CGFloat((refWidth! - Int(toSize.width)) / 2) + let y = CGFloat((refHeight! - Int(toSize.height)) / 2) - let cropRect = CGRectMake(x, y, toSize.height, toSize.width) - let imageRef = CGImageCreateWithImageInRect(image.CGImage, cropRect)! as CGImageRef + let cropRect = CGRect(x: x, y: y, width: toSize.height, height: toSize.width) + let imageRef = (image.cgImage?.cropping(to: cropRect)!)! as CGImage - let cropped = UIImage(CGImage: imageRef, scale: 0.0, orientation: UIImageOrientation.Up) + let cropped = UIImage(cgImage: imageRef, scale: 0.0, orientation: UIImageOrientation.up) return cropped } - public func addSelectedModel(model:PHAsset, animated:Bool) { + open func addSelectedModel(_ model:PHAsset, animated:Bool) { self.selectedAssets.append((model,animated)) self.delegate?.thumbnailSelectedUpdated(self.selectedAssets) } - public func removeSelectedModel(model:PHAsset,animated:Bool) { - for (index, element) in self.selectedAssets.enumerate() { + open func removeSelectedModel(_ model:PHAsset,animated:Bool) { + for (index, element) in self.selectedAssets.enumerated() { if element.0 == model { - self.selectedAssets.removeAtIndex(index) + self.selectedAssets.remove(at: index) } } self.delegate?.thumbnailSelectedUpdated(self.selectedAssets) } - public func getSelectedAsImages(completion: (images:[(NSData,Bool)]) -> ()) { + open func getSelectedAsImages(_ completion: @escaping (_ images:[(Data,Bool)]) -> ()) { let arrayModelsForSend = self.selectedAssets - var compliedArray = [(NSData,Bool)]() + var compliedArray = [(Data,Bool)]() var isGif = false - for (_,model) in arrayModelsForSend.enumerate() { - self.imageManager.requestImageDataForAsset(model.0, options: requestOptions, resultHandler: { (data, _, _, info) -> Void in + for (_,model) in arrayModelsForSend.enumerated() { + self.imageManager.requestImageData(for: model.0, options: requestOptions, resultHandler: { (data, _, _, info) -> Void in if data != nil { - let gifMarker = info!["PHImageFileURLKey"] as! NSURL + let gifMarker = info!["PHImageFileURLKey"] as! URL isGif = (gifMarker.pathExtension == "GIF") ? true : false print(isGif) compliedArray.append((data!,isGif)) if compliedArray.count == arrayModelsForSend.count { - completion(images: compliedArray) + completion(compliedArray) } } }) } } - public func dismiss() { + open func dismiss() { self.selectedAssets = [] self.reloadView() } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AAVoiceRecorderView.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AAVoiceRecorderView.swift index 663cb3a326..31b017a318 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AAVoiceRecorderView.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Conversation/Views/AAVoiceRecorderView.swift @@ -23,7 +23,7 @@ class AAVoiceRecorderView: UIView { ////////////////////////////////// weak var binedController : ConversationViewController! - var meterTimer:NSTimer! + var meterTimer:Timer! ////////////////////////////////// @@ -68,28 +68,28 @@ class AAVoiceRecorderView: UIView { self.backgroundColor = appStyle.chatInputFieldBgColor self.timeLabel.text = "0:00" - self.timeLabel.font = UIFont.systemFontOfSize(15) + self.timeLabel.font = UIFont.systemFont(ofSize: 15) self.timeLabel.textColor = appStyle.vcHintColor - self.timeLabel.frame = CGRectMake(29, 12, 50, 20) + self.timeLabel.frame = CGRect(x: 29, y: 12, width: 50, height: 20) self.sliderLabel.text = "Slide to cancel" - self.sliderLabel.font = UIFont.systemFontOfSize(14) - self.sliderLabel.textAlignment = .Left - self.sliderLabel.frame = CGRectMake(140,12,100,20) + self.sliderLabel.font = UIFont.systemFont(ofSize: 14) + self.sliderLabel.textAlignment = .left + self.sliderLabel.frame = CGRect(x: 140,y: 12,width: 100,height: 20) self.sliderLabel.textColor = appStyle.vcHintColor self.sliderArrow.image = UIImage.tinted("aa_recorderarrow", color: appStyle.vcHintColor) - self.sliderArrow.frame = CGRectMake(110,12,20,20) + self.sliderArrow.frame = CGRect(x: 110,y: 12,width: 20,height: 20) self.recorderImageCircle.image = UIImage.tinted("aa_recordercircle", color: UIColor(red: 0.7287, green: 0.7252, blue: 0.7322, alpha: 1.0)) - self.recorderImageCircle.frame = CGRectMake(10, 15, 14, 14) + self.recorderImageCircle.frame = CGRect(x: 10, y: 15, width: 14, height: 14) // } // update location views from track touch position - func updateLocation(offset:CGFloat,slideToRight:Bool) { + func updateLocation(_ offset:CGFloat,slideToRight:Bool) { var sliderLabelFrame = self.sliderLabel.frame sliderLabelFrame.origin.x += offset @@ -133,17 +133,17 @@ class AAVoiceRecorderView: UIView { func startAnimation() { - self.timeLabel.frame = CGRectMake(-129, 12, 50, 20) - self.sliderLabel.frame = CGRectMake(440,12,100,20) - self.sliderArrow.frame = CGRectMake(310,12,20,20) - self.recorderImageCircle.frame = CGRectMake(-110, 15, 14, 14) + self.timeLabel.frame = CGRect(x: -129, y: 12, width: 50, height: 20) + self.sliderLabel.frame = CGRect(x: 440,y: 12,width: 100,height: 20) + self.sliderArrow.frame = CGRect(x: 310,y: 12,width: 20,height: 20) + self.recorderImageCircle.frame = CGRect(x: -110, y: 15, width: 14, height: 14) - UIView.animateWithDuration(1.5, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1.0, options: UIViewAnimationOptions.CurveLinear, animations: { () -> Void in + UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1.0, options: UIViewAnimationOptions.curveLinear, animations: { () -> Void in - self.timeLabel.frame = CGRectMake(29, 12, 50, 20) - self.sliderLabel.frame = CGRectMake(140,12,100,20) - self.sliderArrow.frame = CGRectMake(110,12,20,20) - self.recorderImageCircle.frame = CGRectMake(10, 15, 14, 14) + self.timeLabel.frame = CGRect(x: 29, y: 12, width: 50, height: 20) + self.sliderLabel.frame = CGRect(x: 140,y: 12,width: 100,height: 20) + self.sliderArrow.frame = CGRect(x: 110,y: 12,width: 20,height: 20) + self.recorderImageCircle.frame = CGRect(x: 10, y: 15, width: 14, height: 14) }, completion: { (complite) -> Void in @@ -155,22 +155,22 @@ class AAVoiceRecorderView: UIView { func recordingStarted() { - UIView.animateWithDuration(0.3, animations: { () -> Void in + UIView.animate(withDuration: 0.3, animations: { () -> Void in self.recorderImageCircle.alpha = 0 - }) { (comp) -> Void in + }, completion: { (comp) -> Void in self.recorderImageCircle.image = UIImage.bundled("aa_recordercircle") - UIView.animateWithDuration(0.3, animations: { () -> Void in + UIView.animate(withDuration: 0.3, animations: { () -> Void in self.recorderImageCircle.alpha = 1 }) self.addAnimationsOnRecorderCircle() self.startUpdateTimer() - } + }) } @@ -180,29 +180,29 @@ class AAVoiceRecorderView: UIView { self.recorderImageCircle.layer.removeAllAnimations() self.recorderImageCircle.image = UIImage.tinted("aa_recordercircle", color: UIColor(red: 0.7287, green: 0.7252, blue: 0.7322, alpha: 1.0)) - self.timeLabel.frame = CGRectMake(29, 12, 50, 20) - self.sliderLabel.frame = CGRectMake(140,12,100,20) - self.sliderArrow.frame = CGRectMake(110,12,20,20) - self.recorderImageCircle.frame = CGRectMake(10, 15, 14, 14) + self.timeLabel.frame = CGRect(x: 29, y: 12, width: 50, height: 20) + self.sliderLabel.frame = CGRect(x: 140,y: 12,width: 100,height: 20) + self.sliderArrow.frame = CGRect(x: 110,y: 12,width: 20,height: 20) + self.recorderImageCircle.frame = CGRect(x: 10, y: 15, width: 14, height: 14) } func startUpdateTimer() { - self.meterTimer = NSTimer.scheduledTimerWithTimeInterval(0.1, + self.meterTimer = Timer.scheduledTimer(timeInterval: 0.1, target:self, selector:#selector(AAVoiceRecorderView.updateAudioMeter(_:)), userInfo:nil, repeats:true) } - func updateAudioMeter(timer:NSTimer) { + func updateAudioMeter(_ timer:Timer) { if let recorder = self.binedController?.audioRecorder { let dur = recorder.currentDuration() let minutes = Int(dur / 60) - let seconds = Int(dur % 60) + let seconds = Int(dur.truncatingRemainder(dividingBy: 60)) if seconds < 10 { self.timeLabel.text = "\(minutes):0\(seconds)" @@ -222,7 +222,7 @@ class AAVoiceRecorderView: UIView { circleAnimation.autoreverses = true circleAnimation.fromValue = 1.0 circleAnimation.toValue = 0.1 - self.recorderImageCircle.layer.addAnimation(circleAnimation, forKey: nil) + self.recorderImageCircle.layer.add(circleAnimation, forKey: nil) } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/AADialogsListContentController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/AADialogsListContentController.swift index 543a4d0b46..49927e5a09 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/AADialogsListContentController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/AADialogsListContentController.swift @@ -4,15 +4,15 @@ import Foundation -public class AADialogsListContentController: AAContentTableController, UISearchBarDelegate, UISearchDisplayDelegate { +open class AADialogsListContentController: AAContentTableController, UISearchBarDelegate, UISearchDisplayDelegate { - public var enableDeletion: Bool = true - public var enableSearch: Bool = true + open var enableDeletion: Bool = true + open var enableSearch: Bool = true - public var delegate: AADialogsListContentControllerDelegate! + open var delegate: AADialogsListContentControllerDelegate! public init() { - super.init(style: .Plain) + super.init(style: .plain) unbindOnDissapear = true } @@ -21,7 +21,7 @@ public class AADialogsListContentController: AAContentTableController, UISearchB fatalError("init(coder:) has not been implemented") } - public override func tableDidLoad() { + open override func tableDidLoad() { managedTable.canEditAll = true managedTable.canDeleteAll = true @@ -32,7 +32,7 @@ public class AADialogsListContentController: AAContentTableController, UISearchB if enableSearch { search(AADialogSearchCell.self) { (s) -> () in - s.searchList = Actor.buildSearchDisplayList() + s.searchModel = Actor.buildGlobalSearchModel() s.selectAction = { (itm) -> () in self.delegate?.searchDidTap(self, entity: itm) @@ -51,7 +51,7 @@ public class AADialogsListContentController: AAContentTableController, UISearchB r.animated = true r.displayList = Actor.getDialogsDisplayList() - if r.displayList.getListProcessor() == nil { + if r.displayList.getProcessor() == nil { r.displayList.setListProcessor(AADialogListProcessor()) } @@ -67,7 +67,64 @@ public class AADialogsListContentController: AAContentTableController, UISearchB } r.editAction = { (dialog: ACDialog) -> () in - self.executeSafe(Actor.deleteChatCommandWithPeer(dialog.peer)) + if dialog.peer.isGroup { + let g = Actor.getGroupWithGid(dialog.peer.peerId) + let isChannel = g.groupType == ACGroupType.channel() + self.alertSheet({ (a) in + + // Clear History + if g.isCanClear.get().booleanValue() { + a.action(AALocalized("ActionClearHistory"), closure: { + self.confirmAlertUserDanger("ActionClearHistoryMessage", action: "ActionClearHistoryAction", tapYes: { + self.executeSafe(Actor.clearChatCommand(with: dialog.peer)) + }) + }) + } + + // Delete + if g.isCanLeave.get().booleanValue() && g.isMember.get().booleanValue() { + if isChannel { + a.destructive(AALocalized("ActionLeaveChannel"), closure: { + self.confirmAlertUserDanger("ActionLeaveChannelMessage", action: "ActionLeaveChannelAction", tapYes: { + self.executePromise(Actor.leaveAndDeleteGroup(withGid: dialog.peer.peerId)) + }) + }) + } else { + a.destructive(AALocalized("ActionDeleteAndExit"), closure: { + self.confirmAlertUserDanger("ActionDeleteAndExitMessage", action: "ActionDeleteAndExitAction", tapYes: { + self.executePromise(Actor.leaveAndDeleteGroup(withGid: dialog.peer.peerId)) + }) + }) + } + } else if g.isCanDelete.get().booleanValue() && g.isMember.get().booleanValue(){ + a.destructive(AALocalized(isChannel ? "ActionDeleteChannel" : "ActionDeleteGroup"), closure: { + self.confirmAlertUserDanger(isChannel ? "ActionDeleteChannelMessage" : "ActionDeleteGroupMessage", action: "ActionDelete", tapYes: { + self.executePromise(Actor.deleteGroup(withGid: g.groupId)) + }) + }) + } else { + a.destructive(AALocalized("ActionDelete"), closure: { + self.confirmAlertUserDanger("ActionDeleteMessage", action: "ActionDelete", tapYes: { + self.executeSafe(Actor.deleteChatCommand(with: dialog.peer)) + }) + }) + } + + // Cancel + a.cancel = AALocalized("ActionCancel") + }) + + } else { + self.alertSheet({ (a) in + a.action(AALocalized("ActionClearHistory"), closure: { + self.executeSafe(Actor.clearChatCommand(with: dialog.peer)) + }) + a.destructive(AALocalized("ActionDelete"), closure: { + self.executeSafe(Actor.deleteChatCommand(with: dialog.peer)) + }) + a.cancel = AALocalized("ActionCancel") + }) + } } } } @@ -81,7 +138,7 @@ public class AADialogsListContentController: AAContentTableController, UISearchB } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // Binding empty dialogs placeholder @@ -93,7 +150,7 @@ public class AADialogsListContentController: AAContentTableController, UISearchB self.showPlaceholder() } else { self.hidePlaceholder() - self.navigationItem.leftBarButtonItem = self.editButtonItem() + self.navigationItem.leftBarButtonItem = self.editButtonItem } } }) diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/AADialogsListContentControllerDelegate.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/AADialogsListContentControllerDelegate.swift index dc425faf37..372c4887ee 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/AADialogsListContentControllerDelegate.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/AADialogsListContentControllerDelegate.swift @@ -6,7 +6,7 @@ import Foundation public protocol AADialogsListContentControllerDelegate { - func recentsDidTap(controller: AADialogsListContentController, dialog: ACDialog) -> Bool + func recentsDidTap(_ controller: AADialogsListContentController, dialog: ACDialog) -> Bool - func searchDidTap(controller: AADialogsListContentController, entity: ACSearchEntity) -} \ No newline at end of file + func searchDidTap(_ controller: AADialogsListContentController, entity: ACSearchResult) +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/Cells/AADialogCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/Cells/AADialogCell.swift index f86667f9a8..997c414f50 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/Cells/AADialogCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/Cells/AADialogCell.swift @@ -4,7 +4,7 @@ import UIKit -public class AADialogCell: AATableViewCell, AABindedCell { +open class AADialogCell: AATableViewCell, AABindedCell { // Binding data type @@ -12,48 +12,46 @@ public class AADialogCell: AATableViewCell, AABindedCell { // Hight of cell - public static func bindedCellHeight(table: AAManagedTable, item: ACDialog) -> CGFloat { + open static func bindedCellHeight(_ table: AAManagedTable, item: ACDialog) -> CGFloat { return 76 } // Cached design - private static let counterBgImage = Imaging - .imageWithColor(ActorSDK.sharedActor().style.dialogCounterBgColor, size: CGSizeMake(18, 18)) + fileprivate static let counterBgImage = Imaging + .imageWithColor(ActorSDK.sharedActor().style.dialogCounterBgColor, size: CGSize(width: 18, height: 18)) .roundImage(18) - .resizableImageWithCapInsets(UIEdgeInsetsMake(9, 9, 9, 9)) - private lazy var dialogTextActiveColor = ActorSDK.sharedActor().style.dialogTextActiveColor - private lazy var dialogTextColor = ActorSDK.sharedActor().style.dialogTextColor - private lazy var dialogStatusSending = ActorSDK.sharedActor().style.dialogStatusSending - private lazy var dialogStatusRead = ActorSDK.sharedActor().style.dialogStatusRead - private lazy var dialogStatusReceived = ActorSDK.sharedActor().style.dialogStatusReceived - private lazy var dialogStatusSent = ActorSDK.sharedActor().style.dialogStatusSent - private lazy var dialogStatusError = ActorSDK.sharedActor().style.dialogStatusError - private lazy var dialogAvatarSize = ActorSDK.sharedActor().style.dialogAvatarSize - private lazy var chatIconClock = ActorSDK.sharedActor().style.chatIconClock - private lazy var chatIconCheck2 = ActorSDK.sharedActor().style.chatIconCheck2 - private lazy var chatIconCheck1 = ActorSDK.sharedActor().style.chatIconCheck1 - private lazy var chatIconError = ActorSDK.sharedActor().style.chatIconError + .resizableImage(withCapInsets: UIEdgeInsetsMake(9, 9, 9, 9)) + fileprivate lazy var dialogTextActiveColor = ActorSDK.sharedActor().style.dialogTextActiveColor + fileprivate lazy var dialogTextColor = ActorSDK.sharedActor().style.dialogTextColor + fileprivate lazy var dialogStatusSending = ActorSDK.sharedActor().style.dialogStatusSending + fileprivate lazy var dialogStatusRead = ActorSDK.sharedActor().style.dialogStatusRead + fileprivate lazy var dialogStatusReceived = ActorSDK.sharedActor().style.dialogStatusReceived + fileprivate lazy var dialogStatusSent = ActorSDK.sharedActor().style.dialogStatusSent + fileprivate lazy var dialogStatusError = ActorSDK.sharedActor().style.dialogStatusError + fileprivate lazy var dialogAvatarSize = ActorSDK.sharedActor().style.dialogAvatarSize + fileprivate lazy var chatIconClock = ActorSDK.sharedActor().style.chatIconClock + fileprivate lazy var chatIconCheck2 = ActorSDK.sharedActor().style.chatIconCheck2 + fileprivate lazy var chatIconCheck1 = ActorSDK.sharedActor().style.chatIconCheck1 + fileprivate lazy var chatIconError = ActorSDK.sharedActor().style.chatIconError // Views - private var cellRenderer: AABackgroundCellRenderer! + fileprivate var cellRenderer: AABackgroundCellRenderer! - public let avatarView = AAAvatarView() - public let titleView = YYLabel() - public let messageView = YYLabel() - - public let dateView = YYLabel() - public let statusView = UIImageView() - public let counterView = YYLabel() - public let counterViewBg = UIImageView() - - private var isEditing = false + open let avatarView = AAAvatarView() + open let titleView = YYLabel() + open let messageView = YYLabel() + open let dateView = YYLabel() + open let statusView = UIImageView() + open let counterView = YYLabel() + open let counterViewBg = UIImageView() + // Binding Data - private var bindedItem: ACDialog? + fileprivate var bindedItem: ACDialog? public override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -82,7 +80,7 @@ public class AADialogCell: AATableViewCell, AABindedCell { counterViewBg.image = AADialogCell.counterBgImage - statusView.contentMode = .Center + statusView.contentMode = .center self.contentView.addSubview(avatarView) self.contentView.addSubview(titleView) @@ -97,7 +95,7 @@ public class AADialogCell: AATableViewCell, AABindedCell { fatalError("init(coder:) has not been implemented") } - public func bind(item: ACDialog, table: AAManagedTable, index: Int, totalCount: Int) { + open func bind(_ item: ACDialog, table: AAManagedTable, index: Int, totalCount: Int) { // // Checking dialog rebinding @@ -110,7 +108,7 @@ public class AADialogCell: AATableViewCell, AABindedCell { var isRebind: Bool = false if let b = bindedItem { - if b.peer.isEqual(item.peer).boolValue { + if b.peer.isEqual(item.peer) { isRebind = true } } @@ -155,20 +153,20 @@ public class AADialogCell: AATableViewCell, AABindedCell { // if item.senderId != Actor.myUid() { - self.statusView.hidden = true + self.statusView.isHidden = true } else { if item.isRead() { self.statusView.tintColor = dialogStatusRead self.statusView.image = chatIconCheck2 - self.statusView.hidden = false + self.statusView.isHidden = false } else if item.isReceived() { self.statusView.tintColor = dialogStatusReceived self.statusView.image = chatIconCheck2 - self.statusView.hidden = false + self.statusView.isHidden = false } else { self.statusView.tintColor = dialogStatusSent self.statusView.image = chatIconCheck1 - self.statusView.hidden = false + self.statusView.isHidden = false } } @@ -178,17 +176,17 @@ public class AADialogCell: AATableViewCell, AABindedCell { setNeedsLayout() } - public override func willTransitionToState(state: UITableViewCellStateMask) { - super.willTransitionToState(state) + open override func willTransition(to state: UITableViewCellStateMask) { + super.willTransition(to: state) - if state.contains(UITableViewCellStateMask.ShowingEditControlMask) { + if state.contains(UITableViewCellStateMask.showingEditControlMask) { isEditing = true } else { isEditing = false } } - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() // We expect height == 76; @@ -200,13 +198,13 @@ public class AADialogCell: AATableViewCell, AABindedCell { // Avatar View // let avatarPadding = padding + (50 - dialogAvatarSize) / 2 - avatarView.frame = CGRectMake(avatarPadding, avatarPadding, dialogAvatarSize, dialogAvatarSize) + avatarView.frame = CGRect(x: avatarPadding, y: avatarPadding, width: dialogAvatarSize, height: dialogAvatarSize) // // Title // - let titleFrame = CGRectMake(leftPadding, 16, width - leftPadding - /*paddingRight*/(padding + 50), 21) + let titleFrame = CGRect(x: leftPadding, y: 16, width: width - leftPadding - /*paddingRight*/(padding + 50), height: 21) UIView.performWithoutAnimation { self.titleView.frame = titleFrame } @@ -216,8 +214,8 @@ public class AADialogCell: AATableViewCell, AABindedCell { // Status Icon // - if (!self.statusView.hidden) { - statusView.frame = CGRectMake(leftPadding, 44, 20, 18) + if (!self.statusView.isHidden) { + statusView.frame = CGRect(x: leftPadding, y: 44, width: 20, height: 18) } @@ -228,7 +226,7 @@ public class AADialogCell: AATableViewCell, AABindedCell { if bindedItem != nil { let config = AADialogCellConfig( item: bindedItem!, - isStatusVisible: !statusView.hidden, + isStatusVisible: !statusView.isHidden, titleWidth: titleFrame.width, contentWidth: width) @@ -247,7 +245,7 @@ public class AADialogCell: AATableViewCell, AABindedCell { } } - private func cellRender(config: AADialogCellConfig) -> AADialogCellLayout! { + fileprivate func cellRender(_ config: AADialogCellConfig) -> AADialogCellLayout! { // // Title Layouting @@ -258,7 +256,7 @@ public class AADialogCell: AATableViewCell, AABindedCell { title.yy_color = appStyle.dialogTitleColor let titleContainer = YYTextContainer(size: CGSize(width: config.titleWidth, height: 1000)) titleContainer.maximumNumberOfRows = 1 - titleContainer.truncationType = .End + titleContainer.truncationType = .end let titleLayout = YYTextLayout(container: titleContainer, text: title)! @@ -280,7 +278,7 @@ public class AADialogCell: AATableViewCell, AABindedCell { let counterLayout: YYTextLayout? if config.item.unreadCount > 0 { let counter = NSMutableAttributedString(string: "\(config.item.unreadCount)") - counter.yy_font = UIFont.systemFontOfSize(14) + counter.yy_font = UIFont.systemFont(ofSize: 14) counter.yy_color = appStyle.dialogCounterColor counterLayout = YYTextLayout(containerSize: CGSize(width: 1000, height: 1000), text: counter)! unreadPadding = max(counterLayout!.textBoundingSize.width + 8, 18) @@ -294,8 +292,8 @@ public class AADialogCell: AATableViewCell, AABindedCell { // let message = NSMutableAttributedString(string: Actor.getFormatter().formatDialogText(config.item)) - message.yy_font = UIFont.systemFontOfSize(16) - if config.item.messageType.ordinal() != ACContentType.TEXT().ordinal() { + message.yy_font = UIFont.systemFont(ofSize: 16) + if config.item.messageType.ordinal() != ACContentType.text().ordinal() { message.yy_color = dialogTextActiveColor } else { message.yy_color = dialogTextColor @@ -303,7 +301,7 @@ public class AADialogCell: AATableViewCell, AABindedCell { let messageWidth = config.contentWidth - 76 - 14 - messagePadding - unreadPadding let messageContainer = YYTextContainer(size: CGSize(width: messageWidth, height: 1000)) messageContainer.maximumNumberOfRows = 1 - messageContainer.truncationType = .End + messageContainer.truncationType = .end let messageLayout = YYTextLayout(container: messageContainer, text: message)! @@ -319,14 +317,14 @@ public class AADialogCell: AATableViewCell, AABindedCell { } let dateAtrStr = NSMutableAttributedString(string: dateStr) dateAtrStr.yy_color = appStyle.dialogDateColor - dateAtrStr.yy_font = UIFont.systemFontOfSize(14) + dateAtrStr.yy_font = UIFont.systemFont(ofSize: 14) let dateContainer = YYTextContainer(size: CGSize(width: 60, height: 1000)) let dateLayout = YYTextLayout(container: dateContainer, text: dateAtrStr)! return AADialogCellLayout(titleLayout: titleLayout, messageLayout: messageLayout, messageWidth: messageWidth, counterLayout: counterLayout, dateLayout: dateLayout) } - private func cellApply(render: AADialogCellLayout!) { + fileprivate func cellApply(_ render: AADialogCellLayout!) { // // Avatar @@ -356,7 +354,7 @@ public class AADialogCell: AATableViewCell, AABindedCell { dateView.textLayout = render.dateLayout let dateWidth = render.dateLayout.textBoundingSize.width - dateView.frame = CGRectMake(contentView.width - dateWidth - leftPadding, 18, dateWidth, 18) + dateView.frame = CGRect(x: contentView.width - dateWidth - leftPadding, y: 18, width: dateWidth, height: 18) presentView(dateView) @@ -365,10 +363,10 @@ public class AADialogCell: AATableViewCell, AABindedCell { // var padding: CGFloat = 76 - if !statusView.hidden { + if !statusView.isHidden { padding += 22 } - let messageViewFrame = CGRectMake(padding, 44, render.messageWidth, 18) + let messageViewFrame = CGRect(x: padding, y: 44, width: render.messageWidth, height: 18) UIView.performWithoutAnimation { self.messageView.frame = messageViewFrame } @@ -379,7 +377,7 @@ public class AADialogCell: AATableViewCell, AABindedCell { // // Message State // - if !self.statusView.hidden { + if !self.statusView.isHidden { presentView(self.statusView) } @@ -394,8 +392,8 @@ public class AADialogCell: AATableViewCell, AABindedCell { let textW = render.counterLayout!.textBoundingSize.width let unreadW = max(textW + 8, 18) - counterView.frame = CGRectMake(contentView.width - leftPadding - unreadW + (unreadW - textW) / 2, 44, textW, 18) - counterViewBg.frame = CGRectMake(contentView.width - leftPadding - unreadW, 44, unreadW, 18) + counterView.frame = CGRect(x: contentView.width - leftPadding - unreadW + (unreadW - textW) / 2, y: 44, width: textW, height: 18) + counterViewBg.frame = CGRect(x: contentView.width - leftPadding - unreadW, y: 44, width: unreadW, height: 18) presentView(counterView) presentView(counterViewBg) @@ -406,11 +404,11 @@ public class AADialogCell: AATableViewCell, AABindedCell { } } - private func presentView(view: UIView) { + fileprivate func presentView(_ view: UIView) { view.alpha = 1 } - private func dismissView(view: UIView) { + fileprivate func dismissView(_ view: UIView) { view.alpha = 0 } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/Cells/AADialogListProcessor.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/Cells/AADialogListProcessor.swift index fefaef7a4f..f5b4c11c79 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/Cells/AADialogListProcessor.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Dialogs List/Cells/AADialogListProcessor.swift @@ -6,12 +6,12 @@ import Foundation class AADialogListProcessor: NSObject, ARListProcessor { - func processWithItems(items: JavaUtilList, withPrevious previous: AnyObject?) -> AnyObject? { + func process(withItems items: JavaUtilList, withPrevious previous: Any?) -> Any? { var uids = Set() for i in 0.. CGFloat { + open static func bindedCellHeight(_ item: BindData) -> CGFloat { return 76 } - private let avatarView: AAAvatarView = AAAvatarView() - private let titleView: UILabel = UILabel() + fileprivate let avatarView: AAAvatarView = AAAvatarView() + fileprivate let titleView: UILabel = UILabel() public override init(style: UITableViewCellStyle, reuseIdentifier: String?) { - super.init(style: UITableViewCellStyle.Default, reuseIdentifier: reuseIdentifier) + super.init(style: UITableViewCellStyle.default, reuseIdentifier: reuseIdentifier) titleView.font = UIFont.mediumSystemFontOfSize(19) titleView.textColor = ActorSDK.sharedActor().style.dialogTextColor @@ -31,24 +31,24 @@ public class AADialogSearchCell: AATableViewCell, AABindedSearchCell { fatalError("init(coder:) has not been implemented") } - public func bind(item: ACSearchEntity, search: String?) { + open func bind(_ item: ACSearchResult, search: String?) { avatarView.bind(item.title, id: Int(item.peer.peerId), avatar: item.avatar) titleView.text = item.title } - public override func prepareForReuse() { + open override func prepareForReuse() { super.prepareForReuse() avatarView.unbind() } - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() let width = self.contentView.frame.width let leftPadding = CGFloat(76) let padding = CGFloat(14) - avatarView.frame = CGRectMake(padding, padding, 52, 52) - titleView.frame = CGRectMake(leftPadding, 0, width - leftPadding - (padding + 50), contentView.bounds.size.height) + avatarView.frame = CGRect(x: padding, y: padding, width: 52, height: 52) + titleView.frame = CGRect(x: leftPadding, y: 0, width: width - leftPadding - (padding + 50), height: contentView.bounds.size.height) } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Input/AAEditFieldController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Input/AAEditFieldController.swift index ddaeee44d4..d2b1e471c9 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Input/AAEditFieldController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Input/AAEditFieldController.swift @@ -4,20 +4,20 @@ import Foundation -public class AAEditFieldControllerConfig { +open class AAEditFieldControllerConfig { - public var title: String! - public var actionTitle: String! - public var hint: String! - public var initialText: String! + open var title: String! + open var actionTitle: String! + open var hint: String! + open var initialText: String! - public var fieldReturnKey: UIReturnKeyType = .Default - public var fieldHint: String! - public var fieldAutocorrectionType = UITextAutocorrectionType.Default - public var fieldAutocapitalizationType = UITextAutocapitalizationType.Sentences + open var fieldReturnKey: UIReturnKeyType = .default + open var fieldHint: String! + open var fieldAutocorrectionType = UITextAutocorrectionType.default + open var fieldAutocapitalizationType = UITextAutocapitalizationType.sentences - public var didDismissTap: ((c: AAEditFieldController)->())? - public var didDoneTap: ((t: String, c: AAEditFieldController)->())? + open var didDismissTap: ((_ c: AAEditFieldController)->())? + open var didDoneTap: ((_ t: String, _ c: AAEditFieldController)->())? func check() { if title == nil { @@ -26,25 +26,25 @@ public class AAEditFieldControllerConfig { } } -public class AAEditFieldController: AAContentTableController { +open class AAEditFieldController: AAContentTableController { - public var fieldCell: AAEditRow! + open var fieldCell: AAEditRow! - public let config: AAEditFieldControllerConfig + open let config: AAEditFieldControllerConfig public init(config: AAEditFieldControllerConfig) { self.config = config - super.init(style: .SettingsGrouped) + super.init(style: .settingsGrouped) navigationItem.title = AALocalized(config.title) - navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: .Plain, target: self, action: #selector(AAEditFieldController.doDismiss)) + navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: .plain, target: self, action: #selector(AAEditFieldController.doDismiss)) if config.actionTitle != nil { - navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized(config.actionTitle), style: .Done, target: self, action: #selector(AAEditFieldController.doAction)) + navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized(config.actionTitle), style: .done, target: self, action: #selector(AAEditFieldController.doAction)) } else { - navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationDone"), style: .Done, target: self, action: #selector(AAEditFieldController.doAction)) + navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationDone"), style: .done, target: self, action: #selector(AAEditFieldController.doAction)) } } @@ -52,7 +52,7 @@ public class AAEditFieldController: AAContentTableController { fatalError("init(coder:) has not been implemented") } - public override func tableDidLoad() { + open override func tableDidLoad() { section { (s) -> () in @@ -83,30 +83,30 @@ public class AAEditFieldController: AAContentTableController { } let text = fieldCell.text!.trim() - config.didDoneTap?(t: text, c: self) + config.didDoneTap?(text, self) } func doDismiss() { if config.didDismissTap != nil { - config.didDismissTap!(c: self) + config.didDismissTap!(self) } else { - dismiss() + dismissController() } } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if let c = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0)) as? AAEditCell { + if let c = tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? AAEditCell { c.textField.becomeFirstResponder() } } - public override func viewWillDisappear(animated: Bool) { + open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - if let c = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0)) as? AAEditCell { + if let c = tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? AAEditCell { c.textField.resignFirstResponder() } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Input/AAEditTextController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Input/AAEditTextController.swift index 78ab38ed0f..43b8bc6c4b 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Input/AAEditTextController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Input/AAEditTextController.swift @@ -5,16 +5,16 @@ import Foundation import SZTextView -public class AAEditTextControllerConfig { +open class AAEditTextControllerConfig { - public var title: String! - public var hint: String! - public var actionTitle: String! - public var initialText: String! + open var title: String! + open var hint: String! + open var actionTitle: String! + open var initialText: String! - public var didDismissTap: ((AAEditTextController) -> ())! + open var didDismissTap: ((AAEditTextController) -> ())! - public var didCompleteTap: ((String, AAEditTextController) -> ())! + open var didCompleteTap: ((String, AAEditTextController) -> ())! func check() { @@ -27,11 +27,11 @@ public class AAEditTextControllerConfig { } } -public class AAEditTextController: AAViewController { +open class AAEditTextController: AAViewController { - private let config: AAEditTextControllerConfig + fileprivate let config: AAEditTextControllerConfig - private var textView = SZTextView() + fileprivate var textView = SZTextView() public init(config: AAEditTextControllerConfig) { @@ -42,60 +42,60 @@ public class AAEditTextController: AAViewController { self.navigationItem.title = AALocalized(config.title) if config.actionTitle != nil { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized(config.actionTitle), style: UIBarButtonItemStyle.Done, target: self, action: #selector(AAEditTextController.doSave)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized(config.actionTitle), style: UIBarButtonItemStyle.done, target: self, action: #selector(AAEditTextController.doSave)) } else { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationDone"), style: UIBarButtonItemStyle.Done, target: self, action: #selector(AAEditTextController.doSave)) + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationDone"), style: UIBarButtonItemStyle.done, target: self, action: #selector(AAEditTextController.doSave)) } - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: UIBarButtonItemStyle.Plain, target: self, action: #selector(AAEditTextController.doCancel)) + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: UIBarButtonItemStyle.plain, target: self, action: #selector(AAEditTextController.doCancel)) self.textView.fadeTime = 0 if let h = config.hint { self.textView.placeholder = AALocalized(h) } self.textView.text = config.initialText - self.textView.font = UIFont.systemFontOfSize(18) + self.textView.font = UIFont.systemFont(ofSize: 18) - self.view.backgroundColor = UIColor.whiteColor() + self.view.backgroundColor = UIColor.white } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() self.view.addSubview(textView) } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.textView.becomeFirstResponder() } - public override func viewWillDisappear(animated: Bool) { + open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) self.textView.resignFirstResponder() } - public func doSave() { + open func doSave() { self.config.didCompleteTap?(textView.text, self) } - public func doCancel() { + open func doCancel() { if self.config.didDismissTap != nil { self.config.didDismissTap!(self) } else { - dismiss() + dismissController() } } - public override func viewDidLayoutSubviews() { + open override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - self.textView.frame = CGRectMake(7, 7, self.view.bounds.width - 14, self.view.bounds.height - 14) + self.textView.frame = CGRect(x: 7, y: 7, width: self.view.bounds.width - 14, height: self.view.bounds.height - 14) } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Previews/AACorePreviewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Previews/AACorePreviewController.swift index 4c1d54984e..f18ae48271 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Previews/AACorePreviewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Previews/AACorePreviewController.swift @@ -4,7 +4,7 @@ import Foundation -public class AACodePreviewController: AAViewController { +open class AACodePreviewController: AAViewController { var webView = UIWebView() let code: String @@ -18,7 +18,7 @@ public class AACodePreviewController: AAViewController { fatalError("init(coder:) has not been implemented") } - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() navigationItem.title = "Source Code" @@ -34,17 +34,17 @@ public class AACodePreviewController: AAViewController { "\n" + "" - let bundle = NSBundle.framework - let path = bundle.pathForResource("highlight.min", ofType: "js")! + let bundle = Bundle.framework + let path = bundle.path(forResource: "highlight.min", ofType: "js")! - webView.loadHTMLString(data, baseURL: NSURL(fileURLWithPath: path)) + webView.loadHTMLString(data, baseURL: URL(fileURLWithPath: path)) view.addSubview(webView) } - public override func viewDidLayoutSubviews() { + open override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() webView.frame = view.bounds } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Previews/AAPhotoPreviewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Previews/AAPhotoPreviewController.swift index e7f14f8b0e..24439ef926 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Previews/AAPhotoPreviewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Previews/AAPhotoPreviewController.swift @@ -4,7 +4,7 @@ import Foundation -public class AAPhotoPreviewController: NYTPhotosViewController, NYTPhotosViewControllerDelegate { +open class AAPhotoPreviewController: NYTPhotosViewController, NYTPhotosViewControllerDelegate { var autoShowBadge = false @@ -35,7 +35,7 @@ public class AAPhotoPreviewController: NYTPhotosViewController, NYTPhotosViewCon } if p.file != nil { - let desc = Actor.findDownloadedDescriptorWithFileId(p.file!.getFileId()) + let desc = Actor.findDownloadedDescriptor(withFileId: p.file!.getFileId()) if desc != nil { let img = UIImage(contentsOfFile: CocoaFiles.pathFromDescriptor(desc!)) if img != nil { @@ -46,7 +46,7 @@ public class AAPhotoPreviewController: NYTPhotosViewController, NYTPhotosViewCon } if p.previewFile != nil { - let desc = Actor.findDownloadedDescriptorWithFileId(p.previewFile!.getFileId()) + let desc = Actor.findDownloadedDescriptor(withFileId: p.previewFile!.getFileId()) if desc != nil { var img = UIImage(contentsOfFile: CocoaFiles.pathFromDescriptor(desc!)) if img != nil { @@ -77,11 +77,11 @@ public class AAPhotoPreviewController: NYTPhotosViewController, NYTPhotosViewCon fatalError("init(coder:) has not been implemented") } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // Setting tint color - navigationController?.navigationBar.tintColor = UIColor.whiteColor() + navigationController?.navigationBar.tintColor = UIColor.white // Binding files for i in 0.. Void in cp.image = image - self.updateImageForPhoto(cp) + self.updateImage(for: cp) }) }) - Actor.bindRawFileWithReference(p.file!, autoStart: true, withCallback: callback) + Actor.bindRawFile(with: p.file!, autoStart: true, with: callback) bind[i] = callback } } // Hide Status bar - UIApplication.sharedApplication().animateStatusBarAppearance(.SlideUp, duration: 0.3) + UIApplication.shared.animateStatusBarAppearance(.slideUp, duration: 0.3) // Hide badge if autoShowBadge { @@ -115,21 +115,21 @@ public class AAPhotoPreviewController: NYTPhotosViewController, NYTPhotosViewCon } } - public func photosViewController(photosViewController: NYTPhotosViewController, referenceViewForPhoto photo: NYTPhoto) -> UIView? { + open func photosViewController(_ photosViewController: NYTPhotosViewController, referenceViewFor photo: NYTPhoto) -> UIView? { return self.fromView } - public override func viewWillDisappear(animated: Bool) { + open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) // Unbind all for i in bind { - Actor.unbindRawFileWithFileId(photos[i.0].file!.getFileId(), autoCancel: true, withCallback: i.1) + Actor.unbindRawFile(withFileId: photos[i.0].file!.getFileId(), autoCancel: true, with: i.1) } bind.removeAll() // Restoring status bar - UIApplication.sharedApplication().animateStatusBarAppearance(.SlideDown, duration: 0.3) + UIApplication.shared.animateStatusBarAppearance(.slideDown, duration: 0.3) // Restoring badge if autoShowBadge { @@ -138,7 +138,7 @@ public class AAPhotoPreviewController: NYTPhotosViewController, NYTPhotosViewCon } } -public class PreviewImage { +open class PreviewImage { let preview: UIImage? var image: UIImage? @@ -166,7 +166,7 @@ public class PreviewImage { class AAPhoto: NSObject, NYTPhoto { var image: UIImage? - var imageData: NSData? + var imageData: Data? var placeholderImage: UIImage? let attributedCaptionTitle: NSAttributedString? let attributedCaptionSummary: NSAttributedString? @@ -224,4 +224,4 @@ class AAPhoto: NSObject, NYTPhoto { public var attributedCaptionCredit: NSAttributedString? { get } */ -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Previews/AAWallpapperPreviewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Previews/AAWallpapperPreviewController.swift index bd1e30d2b1..0b1ce59da1 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Previews/AAWallpapperPreviewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/Previews/AAWallpapperPreviewController.swift @@ -4,15 +4,15 @@ import Foundation -public class AAWallpapperPreviewController: AAViewController { +open class AAWallpapperPreviewController: AAViewController { - private let imageView = UIImageView() - private let cancelButton = UIButton() - private let setButton = UIButton() + fileprivate let imageView = UIImageView() + fileprivate let cancelButton = UIButton() + fileprivate let setButton = UIButton() - private let imageName: String - private let selectedImage: UIImage - private var fromName: Bool + fileprivate let imageName: String + fileprivate let selectedImage: UIImage + fileprivate var fromName: Bool public init(imageName: String) { self.imageName = imageName @@ -20,16 +20,16 @@ public class AAWallpapperPreviewController: AAViewController { self.fromName = true super.init() imageView.image = UIImage.bundled(imageName)! - imageView.contentMode = .ScaleAspectFill + imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true cancelButton.backgroundColor = appStyle.vcPanelBgColor - cancelButton.addTarget(self, action: #selector(AAWallpapperPreviewController.cancelDidTap), forControlEvents: .TouchUpInside) - cancelButton.setTitle(AALocalized("AlertCancel"), forState: .Normal) - cancelButton.setTitleColor(appStyle.tabUnselectedTextColor, forState: .Normal) + cancelButton.addTarget(self, action: #selector(AAWallpapperPreviewController.cancelDidTap), for: .touchUpInside) + cancelButton.setTitle(AALocalized("AlertCancel"), for: UIControlState()) + cancelButton.setTitleColor(appStyle.tabUnselectedTextColor, for: UIControlState()) setButton.backgroundColor = appStyle.vcPanelBgColor - setButton.addTarget(self, action: #selector(AAWallpapperPreviewController.setDidTap), forControlEvents: .TouchUpInside) - setButton.setTitle(AALocalized("AlertSet"), forState: .Normal) - setButton.setTitleColor(appStyle.tabUnselectedTextColor, forState: .Normal) + setButton.addTarget(self, action: #selector(AAWallpapperPreviewController.setDidTap), for: .touchUpInside) + setButton.setTitle(AALocalized("AlertSet"), for: UIControlState()) + setButton.setTitleColor(appStyle.tabUnselectedTextColor, for: UIControlState()) } public init(selectedImage: UIImage) { @@ -38,33 +38,33 @@ public class AAWallpapperPreviewController: AAViewController { self.fromName = false super.init() imageView.image = selectedImage - imageView.contentMode = .ScaleAspectFill + imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true cancelButton.backgroundColor = appStyle.vcPanelBgColor - cancelButton.addTarget(self, action: #selector(AAWallpapperPreviewController.cancelDidTap), forControlEvents: .TouchUpInside) - cancelButton.setTitle(AALocalized("AlertCancel"), forState: .Normal) - cancelButton.setTitleColor(appStyle.tabUnselectedTextColor, forState: .Normal) + cancelButton.addTarget(self, action: #selector(AAWallpapperPreviewController.cancelDidTap), for: .touchUpInside) + cancelButton.setTitle(AALocalized("AlertCancel"), for: UIControlState()) + cancelButton.setTitleColor(appStyle.tabUnselectedTextColor, for: UIControlState()) setButton.backgroundColor = appStyle.vcPanelBgColor - setButton.addTarget(self, action: #selector(AAWallpapperPreviewController.setDidTap), forControlEvents: .TouchUpInside) - setButton.setTitle(AALocalized("AlertSet"), forState: .Normal) - setButton.setTitleColor(appStyle.tabUnselectedTextColor, forState: .Normal) + setButton.addTarget(self, action: #selector(AAWallpapperPreviewController.setDidTap), for: .touchUpInside) + setButton.setTitle(AALocalized("AlertSet"), for: UIControlState()) + setButton.setTitleColor(appStyle.tabUnselectedTextColor, for: UIControlState()) } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() - self.edgesForExtendedLayout = UIRectEdge.Top + self.edgesForExtendedLayout = UIRectEdge.top view.addSubview(imageView) view.addSubview(cancelButton) view.addSubview(setButton) } - public override func viewDidLayoutSubviews() { + open override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() imageView.frame = view.bounds @@ -74,11 +74,11 @@ public class AAWallpapperPreviewController: AAViewController { } func cancelDidTap() { - self.dismissViewControllerAnimated(true, completion: nil) + self.dismiss(animated: true, completion: nil) } func setDidTap() { - self.dismissViewControllerAnimated(true, completion: nil) + self.dismiss(animated: true, completion: nil) if self.fromName == true { Actor.changeSelectedWallpaper("local:\(imageName)") @@ -89,7 +89,7 @@ public class AAWallpapperPreviewController: AAViewController { let descriptor = "/tmp/customWallpaperImage" let path = CocoaFiles.pathFromDescriptor(descriptor) - UIImageJPEGRepresentation(self.selectedImage, 1.00)!.writeToFile(path, atomically: true) + try? UIImageJPEGRepresentation(self.selectedImage, 1.00)!.write(to: URL(fileURLWithPath: path), options: [.atomic]) Actor.changeSelectedWallpaper("file:\(descriptor)") }) @@ -97,4 +97,4 @@ public class AAWallpapperPreviewController: AAViewController { } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/WebActions/AAWebActionController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/WebActions/AAWebActionController.swift index 4374e2ad10..32c2244f47 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/WebActions/AAWebActionController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Content/WebActions/AAWebActionController.swift @@ -4,12 +4,12 @@ import Foundation -public class AAWebActionController: AAViewController, UIWebViewDelegate { +open class AAWebActionController: AAViewController, UIWebViewDelegate { - private var webView = UIWebView() + fileprivate var webView = UIWebView() - private let regex: AARegex - private let desc: ACWebActionDescriptor + fileprivate let regex: AARegex + fileprivate let desc: ACWebActionDescriptor public init(desc: ACWebActionDescriptor) { self.desc = desc @@ -21,34 +21,34 @@ public class AAWebActionController: AAViewController, UIWebViewDelegate { fatalError("init(coder:) has not been implemented") } - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() webView.delegate = self view.addSubview(webView) - webView.loadRequest(NSURLRequest(URL: NSURL(string: desc.getUri())!)) + webView.loadRequest(URLRequest(url: URL(string: desc.getUri())!)) } - public override func viewDidLayoutSubviews() { + open override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() webView.frame = view.bounds } - public func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool { - if let url = request.URL { + open func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool { + if let url = request.url { let rawUrl = url.absoluteString // Match end url if regex.test(rawUrl) { - self.executeSafe(Actor.completeWebActionWithHash(desc.getActionHash(), withUrl: rawUrl)) { (val) -> Void in - self.dismiss() + self.executeSafe(Actor.completeWebAction(withHash: desc.getActionHash(), withUrl: rawUrl)) { (val) -> Void in + self.dismissController() } return false } } return true } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Conversation/ConversationViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Conversation/ConversationViewController.swift index a5d0f80c60..0783be8354 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Conversation/ConversationViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Conversation/ConversationViewController.swift @@ -8,7 +8,7 @@ import MobileCoreServices import AddressBook import AddressBookUI -public class ConversationViewController: +open class ConversationViewController: AAConversationContentController, UIDocumentMenuDelegate, UIDocumentPickerDelegate, @@ -21,7 +21,7 @@ public class ConversationViewController: AAStickersKeyboardDelegate { // Data binder - private let binder = AABinder() + fileprivate let binder = AABinder() // Internal state // Members for autocomplete @@ -34,37 +34,38 @@ public class ConversationViewController: // Views // - private let titleView: UILabel = UILabel() - private let subtitleView: UILabel = UILabel() - private let navigationView: UIView = UIView() - private let avatarView = AABarAvatarView() - private let backgroundView = UIImageView() - private var audioButton: UIButton = UIButton() - private var voiceRecorderView : AAVoiceRecorderView! - + fileprivate let titleView: UILabel = UILabel() + fileprivate let subtitleView: UILabel = UILabel() + fileprivate let navigationView: UIView = UIView() + fileprivate let avatarView = AABarAvatarView() + fileprivate let backgroundView = UIImageView() + fileprivate var audioButton: UIButton = UIButton() + fileprivate var voiceRecorderView : AAVoiceRecorderView! + fileprivate let inputOverlay = UIView() + fileprivate let inputOverlayLabel = UILabel() // // Stickers // - private var stickersView: AAStickersKeyboard! - private var stickersButton : UIButton! - private var stickersOpen = false + fileprivate var stickersView: AAStickersKeyboard! + fileprivate var stickersButton : UIButton! + fileprivate var stickersOpen = false // // Audio Recorder // - public var audioRecorder: AAAudioRecorder! + open var audioRecorder: AAAudioRecorder! // // Mode // - private var textMode:Bool! - private var micOn: Bool! = true + fileprivate var textMode:Bool! + fileprivate var micOn: Bool! = true @@ -88,7 +89,7 @@ public class ConversationViewController: // backgroundView.clipsToBounds = true - backgroundView.contentMode = .ScaleAspectFill + backgroundView.contentMode = .scaleAspectFill backgroundView.backgroundColor = appStyle.chatBgColor // Custom background if available @@ -100,15 +101,15 @@ public class ConversationViewController: backgroundView.image = UIImage(contentsOfFile:path) } } - view.insertSubview(backgroundView, atIndex: 0) + view.insertSubview(backgroundView, at: 0) // // slk settings // self.bounces = false - self.keyboardPanningEnabled = true - self.registerPrefixesForAutoCompletion(["@"]) + self.isKeyboardPanningEnabled = true + self.registerPrefixes(forAutoCompletion: ["@"]) // @@ -116,57 +117,68 @@ public class ConversationViewController: // self.textInputbar.backgroundColor = appStyle.chatInputFieldBgColor self.textInputbar.autoHideRightButton = false; - self.textInputbar.translucent = false + self.textInputbar.isTranslucent = false // // Text view // self.textView.placeholder = AALocalized("ChatPlaceholder") - self.textView.keyboardAppearance = ActorSDK.sharedActor().style.isDarkApp ? .Dark : .Light + self.textView.keyboardAppearance = ActorSDK.sharedActor().style.isDarkApp ? .dark : .light + + // + // Overlay + // + self.inputOverlay.addSubview(inputOverlayLabel) + self.inputOverlayLabel.textAlignment = .center + self.inputOverlayLabel.font = UIFont.systemFont(ofSize: 18) + self.inputOverlayLabel.textColor = ActorSDK.sharedActor().style.vcTintColor + self.inputOverlay.viewDidTap = { + self.onOverlayTap() + } // // Add stickers button // - self.stickersButton = UIButton(type: UIButtonType.System) - self.stickersButton.tintColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.5) - self.stickersButton.setImage(UIImage.bundled("sticker_button"), forState: UIControlState.Normal) - self.stickersButton.addTarget(self, action: #selector(ConversationViewController.changeKeyboard), forControlEvents: UIControlEvents.TouchUpInside) + self.stickersButton = UIButton(type: UIButtonType.system) + self.stickersButton.tintColor = UIColor.lightGray.withAlphaComponent(0.5) + self.stickersButton.setImage(UIImage.bundled("sticker_button"), for: UIControlState()) + self.stickersButton.addTarget(self, action: #selector(ConversationViewController.changeKeyboard), for: UIControlEvents.touchUpInside) self.textInputbar.addSubview(stickersButton) // // Check text for set right button // - let checkText = Actor.loadDraftWithPeer(peer)!.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) + let checkText = Actor.loadDraft(with: peer)!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) if (checkText.isEmpty) { self.textMode = false self.rightButton.tintColor = appStyle.chatSendColor - self.rightButton.setImage(UIImage.tinted("aa_micbutton", color: appStyle.chatAttachColor), forState: UIControlState.Normal) - self.rightButton.setTitle("", forState: UIControlState.Normal) - self.rightButton.enabled = true + self.rightButton.setImage(UIImage.tinted("aa_micbutton", color: appStyle.chatAttachColor), for: UIControlState()) + self.rightButton.setTitle("", for: UIControlState()) + self.rightButton.isEnabled = true self.rightButton.layoutIfNeeded() - self.rightButton.addTarget(self, action: #selector(ConversationViewController.beginRecord(_:event:)), forControlEvents: UIControlEvents.TouchDown) - self.rightButton.addTarget(self, action: #selector(ConversationViewController.mayCancelRecord(_:event:)), forControlEvents: UIControlEvents.TouchDragInside.union(UIControlEvents.TouchDragOutside)) - self.rightButton.addTarget(self, action: #selector(ConversationViewController.finishRecord(_:event:)), forControlEvents: UIControlEvents.TouchUpInside.union(UIControlEvents.TouchCancel).union(UIControlEvents.TouchUpOutside)) + self.rightButton.addTarget(self, action: #selector(ConversationViewController.beginRecord(_:event:)), for: UIControlEvents.touchDown) + self.rightButton.addTarget(self, action: #selector(ConversationViewController.mayCancelRecord(_:event:)), for: UIControlEvents.touchDragInside.union(UIControlEvents.touchDragOutside)) + self.rightButton.addTarget(self, action: #selector(ConversationViewController.finishRecord(_:event:)), for: UIControlEvents.touchUpInside.union(UIControlEvents.touchCancel).union(UIControlEvents.touchUpOutside)) } else { self.textMode = true - self.stickersButton.hidden = true + self.stickersButton.isHidden = true - self.rightButton.setTitle(AALocalized("ChatSend"), forState: UIControlState.Normal) - self.rightButton.setTitleColor(appStyle.chatSendColor, forState: UIControlState.Normal) - self.rightButton.setTitleColor(appStyle.chatSendDisabledColor, forState: UIControlState.Disabled) - self.rightButton.setImage(nil, forState: UIControlState.Normal) - self.rightButton.enabled = true + self.rightButton.setTitle(AALocalized("ChatSend"), for: UIControlState()) + self.rightButton.setTitleColor(appStyle.chatSendColor, for: UIControlState()) + self.rightButton.setTitleColor(appStyle.chatSendDisabledColor, for: UIControlState.disabled) + self.rightButton.setImage(nil, for: UIControlState()) + self.rightButton.isEnabled = true self.rightButton.layoutIfNeeded() } @@ -178,28 +190,28 @@ public class ConversationViewController: self.audioRecorder = AAAudioRecorder() self.audioRecorder.delegate = self - self.leftButton.setImage(UIImage.tinted("conv_attach", color: appStyle.chatAttachColor), forState: UIControlState.Normal) + self.leftButton.setImage(UIImage.tinted("conv_attach", color: appStyle.chatAttachColor), for: UIControlState()) // // Navigation Title // - navigationView.frame = CGRectMake(0, 0, 200, 44) - navigationView.autoresizingMask = UIViewAutoresizing.FlexibleWidth + navigationView.frame = CGRect(x: 0, y: 0, width: 200, height: 44) + navigationView.autoresizingMask = UIViewAutoresizing.flexibleWidth titleView.font = UIFont.mediumSystemFontOfSize(17) titleView.adjustsFontSizeToFitWidth = false - titleView.textAlignment = NSTextAlignment.Center - titleView.lineBreakMode = NSLineBreakMode.ByTruncatingTail - titleView.autoresizingMask = UIViewAutoresizing.FlexibleWidth + titleView.textAlignment = NSTextAlignment.center + titleView.lineBreakMode = NSLineBreakMode.byTruncatingTail + titleView.autoresizingMask = UIViewAutoresizing.flexibleWidth titleView.textColor = appStyle.navigationTitleColor - subtitleView.font = UIFont.systemFontOfSize(13) + subtitleView.font = UIFont.systemFont(ofSize: 13) subtitleView.adjustsFontSizeToFitWidth = true - subtitleView.textAlignment = NSTextAlignment.Center - subtitleView.lineBreakMode = NSLineBreakMode.ByTruncatingTail - subtitleView.autoresizingMask = UIViewAutoresizing.FlexibleWidth + subtitleView.textAlignment = NSTextAlignment.center + subtitleView.lineBreakMode = NSLineBreakMode.byTruncatingTail + subtitleView.autoresizingMask = UIViewAutoresizing.flexibleWidth navigationView.addSubview(titleView) navigationView.addSubview(subtitleView) @@ -210,7 +222,7 @@ public class ConversationViewController: // // Navigation Avatar // - avatarView.frame = CGRectMake(0, 0, 40, 40) + avatarView.frame = CGRect(x: 0, y: 0, width: 40, height: 40) avatarView.viewDidTap = onAvatarTap let barItem = UIBarButtonItem(customView: avatarView) @@ -246,42 +258,49 @@ public class ConversationViewController: fatalError("init(coder:) has not been implemented") } - override public func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() - self.voiceRecorderView = AAVoiceRecorderView(frame: CGRectMake(0,0,self.view.frame.size.width-30,44)) - self.voiceRecorderView.hidden = true + self.voiceRecorderView = AAVoiceRecorderView(frame: CGRect(x: 0, y: 0, width: view.width - 30, height: 44)) + self.voiceRecorderView.isHidden = true self.voiceRecorderView.binedController = self self.textInputbar.addSubview(self.voiceRecorderView) - navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: UIBarButtonItemStyle.Plain, target: nil, action: nil) + self.inputOverlay.backgroundColor = UIColor.white + self.inputOverlay.isHidden = false + self.textInputbar.addSubview(self.inputOverlay) - let frame = CGRectMake(0, 0, self.view.frame.size.width, 216) + navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: UIBarButtonItemStyle.plain, target: nil, action: nil) + + let frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 216) self.stickersView = AAStickersKeyboard(frame: frame) self.stickersView.delegate = self - NSNotificationCenter.defaultCenter().addObserver( + NotificationCenter.default.addObserver( self, selector: #selector(ConversationViewController.updateStickersStateOnCloseKeyboard), - name: SLKKeyboardWillHideNotification, + name: NSNotification.Name.SLKKeyboardWillHide, object: nil) } - public override func viewDidLayoutSubviews() { + open override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - self.stickersButton.frame = CGRectMake(self.view.frame.size.width-67, 12, 20, 20) + self.stickersButton.frame = CGRect(x: self.view.frame.size.width-67, y: 12, width: 20, height: 20) + self.voiceRecorderView.frame = CGRect(x: 0, y: 0, width: view.width - 30, height: 44) + self.inputOverlay.frame = CGRect(x: 0, y: 0, width: view.width, height: 44) + self.inputOverlayLabel.frame = CGRect(x: 0, y: 0, width: view.width, height: 44) } //////////////////////////////////////////////////////////// // MARK: - Lifecycle //////////////////////////////////////////////////////////// - override public func viewWillAppear(animated: Bool) { + override open func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // Installing bindings - if (peer.peerType.ordinal() == ACPeerType.PRIVATE().ordinal()) { + if (peer.peerType.ordinal() == ACPeerType.private().ordinal()) { let user = Actor.getUserWithUid(peer.peerId) let nameModel = user.getNameModel() @@ -301,14 +320,14 @@ public class ConversationViewController: self.subtitleView.text = Actor.getFormatter().formatTyping() self.subtitleView.textColor = self.appStyle.navigationSubtitleActiveColor } else { - if (user.isBot().boolValue) { + if (user.isBot()) { self.subtitleView.text = "bot" self.subtitleView.textColor = self.appStyle.userOnlineNavigationColor } else { - let stateText = Actor.getFormatter().formatPresence(presence, withSex: user.getSex()) + let stateText = Actor.getFormatter().formatPresence(presence, with: user.getSex()) self.subtitleView.text = stateText; let state = presence!.state.ordinal() - if (state == ACUserPresence_State.ONLINE().ordinal()) { + if (state == ACUserPresence_State.online().ordinal()) { self.subtitleView.textColor = self.appStyle.userOnlineNavigationColor } else { self.subtitleView.textColor = self.appStyle.userOfflineNavigationColor @@ -316,7 +335,9 @@ public class ConversationViewController: } } }) - } else if (peer.peerType.ordinal() == ACPeerType.GROUP().ordinal()) { + + self.inputOverlay.isHidden = true + } else if (peer.peerType.ordinal() == ACPeerType.group().ordinal()) { let group = Actor.getGroupWithGid(peer.peerId) let nameModel = group.getNameModel() @@ -327,65 +348,122 @@ public class ConversationViewController: binder.bind(group.getAvatarModel(), closure: { (value: ACAvatar?) -> () in self.avatarView.bind(group.getNameModel().get(), id: Int(group.getId()), avatar: value) }) - binder.bind(Actor.getGroupTypingWithGid(group.getId()), valueModel2: group.getMembersModel(), valueModel3: group.getPresenceModel(), closure: { (typingValue:IOSIntArray?, members:JavaUtilHashSet?, onlineCount:JavaLangInteger?) -> () in + binder.bind(Actor.getGroupTyping(withGid: group.getId()), valueModel2: group.membersCount, valueModel3: group.getPresenceModel(), closure: { (typingValue:IOSIntArray?, membersCount: JavaLangInteger?, onlineCount:JavaLangInteger?) -> () in if (!group.isMemberModel().get().booleanValue()) { self.subtitleView.text = AALocalized("ChatNoGroupAccess") self.subtitleView.textColor = self.appStyle.navigationSubtitleColor - self.setTextInputbarHidden(true, animated: true) return - } else { - self.setTextInputbarHidden(false, animated: false) } if (typingValue != nil && typingValue!.length() > 0) { self.subtitleView.textColor = self.appStyle.navigationSubtitleActiveColor if (typingValue!.length() == 1) { - let uid = typingValue!.intAtIndex(0); + let uid = typingValue!.int(at: 0); let user = Actor.getUserWithUid(uid) - self.subtitleView.text = Actor.getFormatter().formatTypingWithName(user.getNameModel().get()) + self.subtitleView.text = Actor.getFormatter().formatTyping(withName: user.getNameModel().get()) } else { - self.subtitleView.text = Actor.getFormatter().formatTypingWithCount(typingValue!.length()); + self.subtitleView.text = Actor.getFormatter().formatTyping(withCount: typingValue!.length()); } } else { - var membersString = Actor.getFormatter().formatGroupMembers(members!.size()) + var membersString = Actor.getFormatter().formatGroupMembers(membersCount!.intValue()) self.subtitleView.textColor = self.appStyle.navigationSubtitleColor - if (onlineCount == nil || onlineCount!.integerValue == 0) { + if (onlineCount == nil || onlineCount!.intValue == 0) { self.subtitleView.text = membersString; } else { - membersString = membersString + ", "; + membersString = membersString! + ", "; let onlineString = Actor.getFormatter().formatGroupOnline(onlineCount!.intValue()); - let attributedString = NSMutableAttributedString(string: (membersString + onlineString)) - attributedString.addAttribute(NSForegroundColorAttributeName, value: self.appStyle.userOnlineNavigationColor, range: NSMakeRange(membersString.length, onlineString.length)) + let attributedString = NSMutableAttributedString(string: (membersString! + onlineString!)) + attributedString.addAttribute(NSForegroundColorAttributeName, value: self.appStyle.userOnlineNavigationColor, range: NSMakeRange(membersString!.length, onlineString!.length)) self.subtitleView.attributedText = attributedString } } }) + + binder.bind(group.isMember, valueModel2: group.isCanWriteMessage, valueModel3: group.isCanJoin, closure: { (isMember: JavaLangBoolean?, canWriteMessage: JavaLangBoolean?, canJoin: JavaLangBoolean?) in + + if canWriteMessage!.booleanValue() { + self.stickersButton.isHidden = false + self.inputOverlay.isHidden = true + } else { + if !isMember!.booleanValue() { + if canJoin!.booleanValue() { + self.inputOverlayLabel.text = AALocalized("ChatJoin") + } else { + self.inputOverlayLabel.text = AALocalized("ChatNoGroupAccess") + } + } else { + if Actor.isNotificationsEnabled(with: self.peer) { + self.inputOverlayLabel.text = AALocalized("ActionMute") + } else { + self.inputOverlayLabel.text = AALocalized("ActionUnmute") + } + } + self.stickersButton.isHidden = true + self.stopAudioRecording() + self.textInputbar.textView.text = "" + self.inputOverlay.isHidden = false + } + }) + + + binder.bind(group.isDeleted) { (isDeleted: JavaLangBoolean?) in + if isDeleted!.booleanValue() { + self.alertUser(AALocalized("ChatDeleted")) { + self.execute(Actor.deleteChatCommand(with: self.peer), successBlock: { (r) in + self.navigateBack() + }) + } + } + } } - Actor.onConversationOpenWithPeer(peer) + Actor.onConversationOpen(with: peer) ActorSDK.sharedActor().trackPageVisible(content) - if textView.isFirstResponder() == false { + if textView.isFirstResponder == false { textView.resignFirstResponder() } - textView.text = Actor.loadDraftWithPeer(peer) + textView.text = Actor.loadDraft(with: peer) } - override public func viewWillLayoutSubviews() { + open func onOverlayTap() { + if peer.isGroup { + let group = Actor.getGroupWithGid(peer.peerId) + if !group.isMember.get().booleanValue() { + if group.isCanJoin.get().booleanValue() { + executePromise(Actor.joinGroup(withGid: peer.peerId)) + } else { + // DO NOTHING + } + } else if !group.isCanWriteMessage.get().booleanValue() { + if Actor.isNotificationsEnabled(with: peer) { + Actor.changeNotificationsEnabled(with: peer, withValue: false) + inputOverlayLabel.text = AALocalized("ActionUnmute") + } else { + Actor.changeNotificationsEnabled(with: peer, withValue: true) + inputOverlayLabel.text = AALocalized("ActionMute") + } + } + } else if peer.isPrivate { + + } + } + + override open func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() backgroundView.frame = view.bounds - titleView.frame = CGRectMake(0, 4, (navigationView.frame.width - 0), 20) - subtitleView.frame = CGRectMake(0, 22, (navigationView.frame.width - 0), 20) + titleView.frame = CGRect(x: 0, y: 4, width: (navigationView.frame.width - 0), height: 20) + subtitleView.frame = CGRect(x: 0, y: 22, width: (navigationView.frame.width - 0), height: 20) - stickersView.frame = CGRectMake(0, 0, self.view.frame.size.width, 216) + stickersView.frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 216) } - override public func viewDidAppear(animated: Bool) { + override open func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if navigationController!.viewControllers.count > 2 { @@ -399,10 +477,10 @@ public class ConversationViewController: } } - override public func viewWillDisappear(animated: Bool) { + override open func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - Actor.onConversationClosedWithPeer(peer) + Actor.onConversationClosed(with: peer) ActorSDK.sharedActor().trackPageHidden(content) if !AADevice.isiPad { @@ -413,10 +491,10 @@ public class ConversationViewController: self.textView.resignFirstResponder() } - override public func viewDidDisappear(animated: Bool) { + override open func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - Actor.saveDraftWithPeer(peer, withDraft: textView.text) + Actor.saveDraft(with: peer, withDraft: textView.text) // Releasing bindings binder.unbindAll() @@ -429,12 +507,12 @@ public class ConversationViewController: func onAvatarTap() { let id = Int(peer.peerId) var controller: AAViewController! - if (peer.peerType.ordinal() == ACPeerType.PRIVATE().ordinal()) { + if (peer.peerType.ordinal() == ACPeerType.private().ordinal()) { controller = ActorSDK.sharedActor().delegate.actorControllerForUser(id) if controller == nil { controller = AAUserViewController(uid: id) } - } else if (peer.peerType.ordinal() == ACPeerType.GROUP().ordinal()) { + } else if (peer.peerType.ordinal() == ACPeerType.group().ordinal()) { controller = ActorSDK.sharedActor().delegate.actorControllerForGroup(id) if controller == nil { controller = AAGroupViewController(gid: id) @@ -448,8 +526,8 @@ public class ConversationViewController: navigation.viewControllers = [controller] let popover = UIPopoverController(contentViewController: navigation) controller.popover = popover - popover.presentPopoverFromBarButtonItem(navigationItem.rightBarButtonItem!, - permittedArrowDirections: UIPopoverArrowDirection.Up, + popover.present(from: navigationItem.rightBarButtonItem!, + permittedArrowDirections: UIPopoverArrowDirection.up, animated: true) } else { navigateNext(controller, removeCurrent: false) @@ -458,15 +536,15 @@ public class ConversationViewController: func onCallTap() { if (self.peer.isGroup) { - execute(ActorSDK.sharedActor().messenger.doCallWithGid(self.peer.peerId)) + execute(ActorSDK.sharedActor().messenger.doCall(withGid: self.peer.peerId)) } else if (self.peer.isPrivate) { - execute(ActorSDK.sharedActor().messenger.doCallWithUid(self.peer.peerId)) + execute(ActorSDK.sharedActor().messenger.doCall(withUid: self.peer.peerId)) } } func onVideoCallTap() { if (self.peer.isPrivate) { - execute(ActorSDK.sharedActor().messenger.doVideoCallWithUid(self.peer.peerId)) + execute(ActorSDK.sharedActor().messenger.doVideoCall(withUid: self.peer.peerId)) } } @@ -474,33 +552,33 @@ public class ConversationViewController: // MARK: - Text bar actions //////////////////////////////////////////////////////////// - override public func textDidUpdate(animated: Bool) { + override open func textDidUpdate(_ animated: Bool) { super.textDidUpdate(animated) - Actor.onTypingWithPeer(peer) + Actor.onTyping(with: peer) checkTextInTextView() } func checkTextInTextView() { - let text = self.textView.text.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) - self.rightButton.enabled = true + let text = self.textView.text.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + self.rightButton.isEnabled = true //change button's if !text.isEmpty && textMode == false { - self.rightButton.removeTarget(self, action: #selector(ConversationViewController.beginRecord(_:event:)), forControlEvents: UIControlEvents.TouchDown) - self.rightButton.removeTarget(self, action: #selector(ConversationViewController.mayCancelRecord(_:event:)), forControlEvents: UIControlEvents.TouchDragInside.union(UIControlEvents.TouchDragOutside)) - self.rightButton.removeTarget(self, action: #selector(ConversationViewController.finishRecord(_:event:)), forControlEvents: UIControlEvents.TouchUpInside.union(UIControlEvents.TouchCancel).union(UIControlEvents.TouchUpOutside)) + self.rightButton.removeTarget(self, action: #selector(ConversationViewController.beginRecord(_:event:)), for: UIControlEvents.touchDown) + self.rightButton.removeTarget(self, action: #selector(ConversationViewController.mayCancelRecord(_:event:)), for: UIControlEvents.touchDragInside.union(UIControlEvents.touchDragOutside)) + self.rightButton.removeTarget(self, action: #selector(ConversationViewController.finishRecord(_:event:)), for: UIControlEvents.touchUpInside.union(UIControlEvents.touchCancel).union(UIControlEvents.touchUpOutside)) self.rebindRightButton() - self.stickersButton.hidden = true + self.stickersButton.isHidden = true - self.rightButton.setTitle(AALocalized("ChatSend"), forState: UIControlState.Normal) - self.rightButton.setTitleColor(appStyle.chatSendColor, forState: UIControlState.Normal) - self.rightButton.setTitleColor(appStyle.chatSendDisabledColor, forState: UIControlState.Disabled) - self.rightButton.setImage(nil, forState: UIControlState.Normal) + self.rightButton.setTitle(AALocalized("ChatSend"), for: UIControlState()) + self.rightButton.setTitleColor(appStyle.chatSendColor, for: UIControlState()) + self.rightButton.setTitleColor(appStyle.chatSendDisabledColor, for: UIControlState.disabled) + self.rightButton.setImage(nil, for: UIControlState()) self.rightButton.layoutIfNeeded() self.textInputbar.layoutIfNeeded() @@ -509,17 +587,17 @@ public class ConversationViewController: } else if (text.isEmpty && textMode == true) { - self.rightButton.addTarget(self, action: #selector(ConversationViewController.beginRecord(_:event:)), forControlEvents: UIControlEvents.TouchDown) - self.rightButton.addTarget(self, action: #selector(ConversationViewController.mayCancelRecord(_:event:)), forControlEvents: UIControlEvents.TouchDragInside.union(UIControlEvents.TouchDragOutside)) - self.rightButton.addTarget(self, action: #selector(ConversationViewController.finishRecord(_:event:)), forControlEvents: UIControlEvents.TouchUpInside.union(UIControlEvents.TouchCancel).union(UIControlEvents.TouchUpOutside)) + self.rightButton.addTarget(self, action: #selector(ConversationViewController.beginRecord(_:event:)), for: UIControlEvents.touchDown) + self.rightButton.addTarget(self, action: #selector(ConversationViewController.mayCancelRecord(_:event:)), for: UIControlEvents.touchDragInside.union(UIControlEvents.touchDragOutside)) + self.rightButton.addTarget(self, action: #selector(ConversationViewController.finishRecord(_:event:)), for: UIControlEvents.touchUpInside.union(UIControlEvents.touchCancel).union(UIControlEvents.touchUpOutside)) - self.stickersButton.hidden = false + self.stickersButton.isHidden = false self.rightButton.tintColor = appStyle.chatAttachColor - self.rightButton.setImage(UIImage.bundled("aa_micbutton"), forState: UIControlState.Normal) - self.rightButton.setTitle("", forState: UIControlState.Normal) - self.rightButton.enabled = true + self.rightButton.setImage(UIImage.bundled("aa_micbutton"), for: UIControlState()) + self.rightButton.setTitle("", for: UIControlState()) + self.rightButton.isEnabled = true self.rightButton.layoutIfNeeded() @@ -535,14 +613,14 @@ public class ConversationViewController: // MARK: - Right/Left button pressed //////////////////////////////////////////////////////////// - override public func didPressRightButton(sender: AnyObject!) { + override open func didPressRightButton(_ sender: Any!) { if !self.textView.text.isEmpty { - Actor.sendMessageWithMentionsDetect(peer, withText: textView.text) + Actor.sendMessage(withMentionsDetect: peer, withText: textView.text) super.didPressRightButton(sender) } } - override public func didPressLeftButton(sender: AnyObject!) { + override open func didPressLeftButton(_ sender: Any!) { super.didPressLeftButton(sender) self.textInputbar.textView.resignFirstResponder() @@ -563,16 +641,16 @@ public class ConversationViewController: // MARK: - Completition //////////////////////////////////////////////////////////// - override public func didChangeAutoCompletionPrefix(prefix: String!, andWord word: String!) { - if self.peer.peerType.ordinal() == ACPeerType.GROUP().ordinal() { + override open func didChangeAutoCompletionPrefix(_ prefix: String!, andWord word: String!) { + if self.peer.peerType.ordinal() == ACPeerType.group().ordinal() { if prefix == "@" { let oldCount = filteredMembers.count - filteredMembers.removeAll(keepCapacity: true) + filteredMembers.removeAll(keepingCapacity: true) - let res = Actor.findMentionsWithGid(self.peer.peerId, withQuery: word) + let res = Actor.findMentions(withGid: self.peer.peerId, withQuery: word)! for index in 0.. Int { + override open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return filteredMembers.count } - override public func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { - let res = AAAutoCompleteCell(style: UITableViewCellStyle.Default, reuseIdentifier: "user_name") - res.bindData(filteredMembers[indexPath.row], highlightWord: foundWord) + override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let res = AAAutoCompleteCell(style: UITableViewCellStyle.default, reuseIdentifier: "user_name") + res.bindData(filteredMembers[(indexPath as NSIndexPath).row], highlightWord: foundWord) return res } - override public func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { - let user = filteredMembers[indexPath.row] + override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let user = filteredMembers[(indexPath as NSIndexPath).row] var postfix = " " if foundPrefixRange.location == 0 { postfix = ": " } - acceptAutoCompletionWithString(user.mentionString + postfix, keepPrefix: !user.isNickname) + acceptAutoCompletion(with: user.mentionString + postfix, keepPrefix: !user.isNickname) } - override public func heightForAutoCompletionView() -> CGFloat { + override open func heightForAutoCompletionView() -> CGFloat { let cellHeight: CGFloat = 44.0; return cellHeight * CGFloat(filteredMembers.count) } - override public func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) { - cell.separatorInset = UIEdgeInsetsZero + override open func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + cell.separatorInset = UIEdgeInsets.zero cell.preservesSuperviewLayoutMargins = false - cell.layoutMargins = UIEdgeInsetsZero + cell.layoutMargins = UIEdgeInsets.zero } //////////////////////////////////////////////////////////// // MARK: - Picker //////////////////////////////////////////////////////////// - public func actionSheetPickedImages(images:[(NSData,Bool)]) { + open func actionSheetPickedImages(_ images:[(Data,Bool)]) { for (i,j) in images { Actor.sendUIImage(i, peer: peer, animated:j) } } - public func actionSheetPickCamera() { - pickImage(.Camera) + open func actionSheetPickCamera() { + pickImage(.camera) } - public func actionSheetPickGallery() { - pickImage(.PhotoLibrary) + open func actionSheetPickGallery() { + pickImage(.photoLibrary) } - public func actionSheetCustomButton(index: Int) { + open func actionSheetCustomButton(_ index: Int) { if index == 0 { pickDocument() } else if index == 1 { @@ -655,62 +733,62 @@ public class ConversationViewController: } } - public func pickContact() { + open func pickContact() { let pickerController = ABPeoplePickerNavigationController() pickerController.peoplePickerDelegate = self - self.presentViewController(pickerController, animated: true, completion: nil) + self.present(pickerController, animated: true, completion: nil) } - public func pickLocation() { + open func pickLocation() { let pickerController = AALocationPickerController() pickerController.delegate = self - self.presentViewController(AANavigationController(rootViewController:pickerController), animated: true, completion: nil) + self.present(AANavigationController(rootViewController:pickerController), animated: true, completion: nil) } - public func pickDocument() { - let documentPicker = UIDocumentMenuViewController(documentTypes: UTTAll as! [String], inMode: UIDocumentPickerMode.Import) - documentPicker.view.backgroundColor = UIColor.clearColor() + open func pickDocument() { + let documentPicker = UIDocumentMenuViewController(documentTypes: UTTAll as [String], in: UIDocumentPickerMode.import) + documentPicker.view.backgroundColor = UIColor.clear documentPicker.delegate = self - self.presentViewController(documentPicker, animated: true, completion: nil) + self.present(documentPicker, animated: true, completion: nil) } //////////////////////////////////////////////////////////// // MARK: - Document picking //////////////////////////////////////////////////////////// - public func documentMenu(documentMenu: UIDocumentMenuViewController, didPickDocumentPicker documentPicker: UIDocumentPickerViewController) { + open func documentMenu(_ documentMenu: UIDocumentMenuViewController, didPickDocumentPicker documentPicker: UIDocumentPickerViewController) { documentPicker.delegate = self - self.presentViewController(documentPicker, animated: true, completion: nil) + self.present(documentPicker, animated: true, completion: nil) } - public func documentPicker(controller: UIDocumentPickerViewController, didPickDocumentAtURL url: NSURL) { + open func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) { // Loading path and file name - let path = url.path! + let path = url.path let fileName = url.lastPathComponent // Check if file valid or directory var isDir : ObjCBool = false - if !NSFileManager.defaultManager().fileExistsAtPath(path, isDirectory: &isDir) { + if !FileManager.default.fileExists(atPath: path, isDirectory: &isDir) { // Not exists return } // Destination file - let descriptor = "/tmp/\(NSUUID().UUIDString)" + let descriptor = "/tmp/\(UUID().uuidString)" let destPath = CocoaFiles.pathFromDescriptor(descriptor) - if isDir { + if isDir.boolValue { // Zipping contents and sending execute(AATools.zipDirectoryCommand(path, to: destPath)) { (val) -> Void in - Actor.sendDocumentWithPeer(self.peer, withName: fileName, withMime: "application/zip", withDescriptor: descriptor) + Actor.sendDocument(with: self.peer, withName: fileName, withMime: "application/zip", withDescriptor: descriptor) } } else { // Sending file itself execute(AATools.copyFileCommand(path, to: destPath)) { (val) -> Void in - Actor.sendDocumentWithPeer(self.peer, withName: fileName, withMime: "application/octet-stream", withDescriptor: descriptor) + Actor.sendDocument(with: self.peer, withName: fileName, withMime: "application/octet-stream", withDescriptor: descriptor) } } } @@ -720,24 +798,24 @@ public class ConversationViewController: // MARK: - Image picking //////////////////////////////////////////////////////////// - func pickImage(source: UIImagePickerControllerSourceType) { + func pickImage(_ source: UIImagePickerControllerSourceType) { let pickerController = AAImagePickerController() pickerController.sourceType = source pickerController.mediaTypes = [kUTTypeImage as String,kUTTypeMovie as String] pickerController.delegate = self - self.presentViewController(pickerController, animated: true, completion: nil) + self.present(pickerController, animated: true, completion: nil) } - public func imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) { - picker.dismissViewControllerAnimated(true, completion: nil) + open func imagePickerController(_ picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) { + picker.dismiss(animated: true, completion: nil) let imageData = UIImageJPEGRepresentation(image, 0.8) Actor.sendUIImage(imageData!, peer: peer, animated:false) } - public func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) { - picker.dismissViewControllerAnimated(true, completion: nil) + open func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { + picker.dismiss(animated: true, completion: nil) if let image = info[UIImagePickerControllerOriginalImage] as? UIImage { let imageData = UIImageJPEGRepresentation(image, 0.8) @@ -745,37 +823,37 @@ public class ConversationViewController: Actor.sendUIImage(imageData!, peer: peer, animated:false) } else { - Actor.sendVideo(info[UIImagePickerControllerMediaURL] as! NSURL, peer: peer) + Actor.sendVideo(info[UIImagePickerControllerMediaURL] as! URL, peer: peer) } } - public func imagePickerControllerDidCancel(picker: UIImagePickerController) { - picker.dismissViewControllerAnimated(true, completion: nil) + open func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true, completion: nil) } //////////////////////////////////////////////////////////// // MARK: - Location picking //////////////////////////////////////////////////////////// - public func locationPickerDidCancelled(controller: AALocationPickerController) { - controller.dismissViewControllerAnimated(true, completion: nil) + open func locationPickerDidCancelled(_ controller: AALocationPickerController) { + controller.dismiss(animated: true, completion: nil) } - public func locationPickerDidPicked(controller: AALocationPickerController, latitude: Double, longitude: Double) { - Actor.sendLocationWithPeer(self.peer, withLongitude: JavaLangDouble(double: longitude), withLatitude: JavaLangDouble(double: latitude), withStreet: nil, withPlace: nil) - controller.dismissViewControllerAnimated(true, completion: nil) + open func locationPickerDidPicked(_ controller: AALocationPickerController, latitude: Double, longitude: Double) { + Actor.sendLocation(with: self.peer, withLongitude: JavaLangDouble(value: longitude), withLatitude: JavaLangDouble(value: latitude), withStreet: nil, withPlace: nil) + controller.dismiss(animated: true, completion: nil) } //////////////////////////////////////////////////////////// // MARK: - Contact picking //////////////////////////////////////////////////////////// - public func peoplePickerNavigationController(peoplePicker: ABPeoplePickerNavigationController, didSelectPerson person: ABRecord) { + open func peoplePickerNavigationController(_ peoplePicker: ABPeoplePickerNavigationController, didSelectPerson person: ABRecord) { // Dismissing picker - peoplePicker.dismissViewControllerAnimated(true, completion: nil) + peoplePicker.dismiss(animated: true, completion: nil) // Names @@ -787,38 +865,38 @@ public class ConversationViewController: let hasAvatarImage = ABPersonHasImageData(person) if (hasAvatarImage) { let imgData = ABPersonCopyImageDataWithFormat(person, kABPersonImageFormatOriginalSize).takeRetainedValue() - let image = UIImage(data: imgData)?.resizeSquare(90, maxH: 90) + let image = UIImage(data: imgData as Data)?.resizeSquare(90, maxH: 90) if (image != nil) { let thumbData = UIImageJPEGRepresentation(image!, 0.55) - jAvatarImage = thumbData?.base64EncodedStringWithOptions(NSDataBase64EncodingOptions()) + jAvatarImage = thumbData?.base64EncodedString(options: NSData.Base64EncodingOptions()) } } // Phones let jPhones = JavaUtilArrayList() - let phoneNumbers: ABMultiValueRef = ABRecordCopyValue(person, kABPersonPhoneProperty).takeRetainedValue() + let phoneNumbers: ABMultiValue = ABRecordCopyValue(person, kABPersonPhoneProperty).takeRetainedValue() let phoneCount = ABMultiValueGetCount(phoneNumbers) for i in 0 ..< phoneCount { let phone = (ABMultiValueCopyValueAtIndex(phoneNumbers, i).takeRetainedValue() as! String).trim() - jPhones.addWithId(phone) + jPhones?.add(withId: phone) } // Email let jEmails = JavaUtilArrayList() - let emails: ABMultiValueRef = ABRecordCopyValue(person, kABPersonEmailProperty).takeRetainedValue() + let emails: ABMultiValue = ABRecordCopyValue(person, kABPersonEmailProperty).takeRetainedValue() let emailsCount = ABMultiValueGetCount(emails) for i in 0 ..< emailsCount { let email = (ABMultiValueCopyValueAtIndex(emails, i).takeRetainedValue() as! String).trim() if (email.length > 0) { - jEmails.addWithId(email) + jEmails?.add(withId: email) } } // Sending - Actor.sendContactWithPeer(self.peer, withName: name!, withPhones: jPhones, withEmails: jEmails, withPhoto: jAvatarImage) + Actor.sendContact(with: self.peer, withName: name!, withPhones: jPhones!, withEmails: jEmails!, withPhoto: jAvatarImage) } @@ -843,25 +921,25 @@ public class ConversationViewController: func onAudioRecordingFinished() { print("onAudioRecordingFinished\n") - audioRecorder.finish { (path: String!, duration: NSTimeInterval) -> Void in + audioRecorder.finish { (path: String?, duration: TimeInterval) -> Void in if (nil == path) { print("onAudioRecordingFinished: empty path") return } - NSLog("onAudioRecordingFinished: %@ [%lfs]", path, duration) - let range = path.rangeOfString("/tmp", options: NSStringCompareOptions(), range: nil, locale: nil) - let descriptor = path.substringFromIndex(range!.startIndex) + NSLog("onAudioRecordingFinished: %@ [%lfs]", path!, duration) + let range = path!.range(of: "/tmp", options: NSString.CompareOptions(), range: nil, locale: nil) + let descriptor = path!.substring(from: range!.lowerBound) NSLog("Audio Recording file: \(descriptor)") - Actor.sendAudioWithPeer(self.peer, withName: NSString.localizedStringWithFormat("%@.ogg", NSUUID().UUIDString) as String, + Actor.sendAudio(with: self.peer, withName: NSString.localizedStringWithFormat("%@.ogg", UUID().uuidString) as String, withDuration: jint(duration*1000), withDescriptor: descriptor) } audioRecorder.cancel() } - public func audioRecorderDidStartRecording() { + open func audioRecorderDidStartRecording() { self.voiceRecorderView.recordingStarted() } @@ -877,16 +955,16 @@ public class ConversationViewController: } } - func beginRecord(button:UIButton,event:UIEvent) { + func beginRecord(_ button:UIButton,event:UIEvent) { self.voiceRecorderView.startAnimation() - self.voiceRecorderView.hidden = false - self.stickersButton.hidden = true + self.voiceRecorderView.isHidden = false + self.stickersButton.isHidden = true - let touches : Set = event.touchesForView(button)! + let touches : Set = event.touches(for: button)! let touch = touches.first! - let location = touch.locationInView(button) + let location = touch.location(in: button) self.voiceRecorderView.trackTouchPoint = location self.voiceRecorderView.firstTouchPoint = location @@ -895,11 +973,11 @@ public class ConversationViewController: self.onAudioRecordingStarted() } - func mayCancelRecord(button:UIButton,event:UIEvent) { + func mayCancelRecord(_ button:UIButton,event:UIEvent) { - let touches : Set = event.touchesForView(button)! + let touches : Set = event.touches(for: button)! let touch = touches.first! - let currentLocation = touch.locationInView(button) + let currentLocation = touch.location(in: button) if (currentLocation.x < self.rightButton.frame.origin.x) { @@ -916,11 +994,11 @@ public class ConversationViewController: if ((self.voiceRecorderView.firstTouchPoint.x - self.voiceRecorderView.trackTouchPoint.x) > 120) { //cancel - self.voiceRecorderView.hidden = true - self.stickersButton.hidden = false + self.voiceRecorderView.isHidden = true + self.stickersButton.isHidden = false self.stopAudioRecording() self.voiceRecorderView.recordingStoped() - button.cancelTrackingWithEvent(event) + button.cancelTracking(with: event) closeRecorderAnimation() @@ -939,7 +1017,7 @@ public class ConversationViewController: let stickerViewFrame = self.stickersButton.frame stickersButton.frame.origin.x = self.stickersButton.frame.origin.x + 500 - UIView.animateWithDuration(1.5, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1.0, options: UIViewAnimationOptions.CurveLinear, animations: { () -> Void in + UIView.animate(withDuration: 1.5, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1.0, options: UIViewAnimationOptions.curveLinear, animations: { () -> Void in self.leftButton.frame = leftButtonFrame self.textView.frame = textViewFrame @@ -953,10 +1031,10 @@ public class ConversationViewController: } - func finishRecord(button:UIButton,event:UIEvent) { + func finishRecord(_ button:UIButton,event:UIEvent) { closeRecorderAnimation() - self.voiceRecorderView.hidden = true - self.stickersButton.hidden = false + self.voiceRecorderView.isHidden = true + self.stickersButton.isHidden = false self.onAudioRecordingFinished() self.voiceRecorderView.recordingStoped() } @@ -967,7 +1045,7 @@ public class ConversationViewController: func updateStickersStateOnCloseKeyboard() { self.stickersOpen = false - self.stickersButton.setImage(UIImage.bundled("sticker_button"), forState: UIControlState.Normal) + self.stickersButton.setImage(UIImage.bundled("sticker_button"), for: UIControlState()) self.textInputbar.textView.inputView = nil } @@ -976,13 +1054,13 @@ public class ConversationViewController: // self.stickersView.loadStickers() self.textInputbar.textView.inputView = self.stickersView - self.textInputbar.textView.inputView?.opaque = false - self.textInputbar.textView.inputView?.backgroundColor = UIColor.clearColor() + self.textInputbar.textView.inputView?.isOpaque = false + self.textInputbar.textView.inputView?.backgroundColor = UIColor.clear self.textInputbar.textView.refreshFirstResponder() self.textInputbar.textView.refreshInputViews() self.textInputbar.textView.becomeFirstResponder() - self.stickersButton.setImage(UIImage.bundled("keyboard_button"), forState: UIControlState.Normal) + self.stickersButton.setImage(UIImage.bundled("keyboard_button"), for: UIControlState()) self.stickersOpen = true } else { @@ -992,7 +1070,7 @@ public class ConversationViewController: self.textInputbar.textView.refreshInputViews() self.textInputbar.textView.becomeFirstResponder() - self.stickersButton.setImage(UIImage.bundled("sticker_button"), forState: UIControlState.Normal) + self.stickersButton.setImage(UIImage.bundled("sticker_button"), for: UIControlState()) self.stickersOpen = false } @@ -1000,8 +1078,8 @@ public class ConversationViewController: self.view.layoutIfNeeded() } - public func stickerDidSelected(keyboard: AAStickersKeyboard, sticker: ACSticker) { - Actor.sendStickerWithPeer(self.peer, withSticker: sticker) + open func stickerDidSelected(_ keyboard: AAStickersKeyboard, sticker: ACSticker) { + Actor.sendSticker(with: self.peer, with: sticker) } } @@ -1015,7 +1093,7 @@ class AABarAvatarView : AAAvatarView { // fatalError("init(coder:) has not been implemented") // } - override func alignmentRectInsets() -> UIEdgeInsets { + override var alignmentRectInsets : UIEdgeInsets { return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8) } } @@ -1029,7 +1107,7 @@ class AACallButton: UIImageView { fatalError("init(coder:) has not been implemented") } - override func alignmentRectInsets() -> UIEdgeInsets { + override var alignmentRectInsets : UIEdgeInsets { return UIEdgeInsets(top: 0, left: -2, bottom: 0, right: 0) } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAAddParticipantViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAAddParticipantViewController.swift index 30a427ef45..ec1d12a89f 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAAddParticipantViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAAddParticipantViewController.swift @@ -4,7 +4,7 @@ import UIKit -public class AAAddParticipantViewController: AAContactsListContentController, AAContactsListContentControllerDelegate { +open class AAAddParticipantViewController: AAContactsListContentController, AAContactsListContentControllerDelegate { public init (gid: Int) { super.init() @@ -17,46 +17,48 @@ public class AAAddParticipantViewController: AAContactsListContentController, AA fatalError("init(coder:) has not been implemented") } - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() title = AALocalized("GroupAddParticipantTitle") - navigationItem.leftBarButtonItem = UIBarButtonItem( - title: AALocalized("NavigationCancel"), - style: UIBarButtonItemStyle.Plain, - target: self, action: #selector(AAViewController.dismiss)) +// navigationItem.leftBarButtonItem = UIBarButtonItem( +// title: AALocalized("NavigationCancel"), +// style: UIBarButtonItemStyle.plain, +// target: self, action: #selector(AAViewController.dismiss)) } - public func willAddContacts(controller: AAContactsListContentController, section: AAManagedSection) { - section.custom { (r:AACustomRow) -> () in - r.height = 56 - r.closure = { (cell) -> () in - cell.bind("ic_invite_user", actionTitle: AALocalized("GroupAddParticipantUrl")) - } - r.selectAction = { () -> Bool in - self.navigateNext(AAInviteLinkViewController(gid: self.gid), removeCurrent: false) - return false + open func willAddContacts(_ controller: AAContactsListContentController, section: AAManagedSection) { + if group.isCanInviteViaLink.get().booleanValue() { + section.custom { (r:AACustomRow) -> () in + r.height = 56 + r.closure = { (cell) -> () in + cell.bind("ic_invite_user", actionTitle: AALocalized("GroupAddParticipantUrl")) + } + r.selectAction = { () -> Bool in + self.navigateNext(AAInviteLinkViewController(gid: self.gid), removeCurrent: false) + return false + } } } } - public func contactDidBind(controller: AAContactsListContentController, contact: ACContact, cell: AAContactCell) { + open func contactDidBind(_ controller: AAContactsListContentController, contact: ACContact, cell: AAContactCell) { cell.bindDisabled(isAlreadyMember(contact.uid)) } - public func contactDidTap(controller: AAContactsListContentController, contact: ACContact) -> Bool { + open func contactDidTap(_ controller: AAContactsListContentController, contact: ACContact) -> Bool { if !isAlreadyMember(contact.uid) { - self.executeSafeOnlySuccess(Actor.inviteMemberCommandWithGid(jint(gid), withUid: jint(contact.uid))!) { (val) -> () in - self.dismiss() + self.executeSafeOnlySuccess(Actor.inviteMemberCommand(withGid: jint(gid), withUid: jint(contact.uid))) { (val) -> () in + self.dismissController() } } return true } - public func isAlreadyMember(uid: jint) -> Bool { - let members: [ACGroupMember] = group.getMembersModel().get().toArray().toSwiftArray() + open func isAlreadyMember(_ uid: jint) -> Bool { + let members: [ACGroupMember] = (group.getMembersModel().get() as AnyObject).toArray().toSwiftArray() for m in members { if m.uid == uid { return true diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAGroupAdministrationViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAGroupAdministrationViewController.swift new file mode 100644 index 0000000000..39a815f10c --- /dev/null +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAGroupAdministrationViewController.swift @@ -0,0 +1,128 @@ +// +// Copyright (c) 2014-2016 Actor LLC. +// + +import Foundation + +open class AAGroupAdministrationViewController: AAContentTableController { + + fileprivate var isChannel: Bool = false + fileprivate var shortNameRow: AACommonRow! + fileprivate var shareHistoryRow: AACommonRow! + + public init(gid: Int) { + super.init(style: .settingsGrouped) + self.gid = gid + self.isChannel = group.groupType == ACGroupType.channel() + navigationItem.title = AALocalized("GroupAdministration") + } + + public required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open override func tableDidLoad() { + + section { (s) in + if isChannel { + s.footerText = AALocalized("GroupPermissionsHintChannel") + } else { + s.footerText = AALocalized("GroupPermissionsHint") + } + self.shortNameRow = s.common({ (r) in + + if (self.isChannel) { + r.content = AALocalized("GroupTypeTitleChannel") + } else { + r.content = AALocalized("GroupTypeTitle") + } + + r.bindAction = { (r) in + if self.group.shortName.get() != nil { + if self.isChannel { + r.hint = AALocalized("ChannelTypePublic") + } else { + r.hint = AALocalized("GroupTypePublic") + } + } else { + if self.isChannel { + r.hint = AALocalized("ChannelTypePrivate") + } else { + r.hint = AALocalized("GroupTypePrivate") + } + } + } + + if group.isCanEditAdministration.get().booleanValue() { + r.style = .navigation + r.selectAction = { () -> Bool in + self.navigateNext(AAGroupTypeViewController(gid: self.gid, isCreation: false)) + return false + } + } + }) + } + + if group.isCanEditAdministration.get().booleanValue() && !isChannel { + section { (s) in + s.footerText = AALocalized("GroupShareHint") + self.shareHistoryRow = s.common({ (r) in + r.content = AALocalized("GroupShareTitle") + r.bindAction = { (r) in + if self.group.isHistoryShared.get().booleanValue() { + r.hint = AALocalized("GroupShareEnabled") + r.selectAction = nil + } else { + r.hint = nil + r.selectAction = { () -> Bool in + self.confirmAlertUser("GroupShareMessage", action: "GroupShareAction", tapYes: { + self.executePromise(Actor.shareHistory(withGid: jint(self.gid))) + }) + return true + } + } + } + }) + } + } + + if group.isCanDelete.get().booleanValue() { + section { (s) in + let action: String + if isChannel { + action = AALocalized("ActionDeleteChannel") + s.footerText = AALocalized("GroupDeleteHintChannel") + } else { + action = AALocalized("ActionDeleteGroup") + s.footerText = AALocalized("GroupDeleteHint") + } + s.danger(action, closure: { (r) in + r.selectAction = { () -> Bool in + self.confirmAlertUserDanger(self.isChannel ? "ActionDeleteChannelMessage" : "ActionDeleteGroupMessage", action: "ActionDelete", tapYes: { + self.executePromise(Actor.deleteGroup(withGid: jint(self.gid))).after { + let first = self.navigationController!.viewControllers.first! + self.navigationController!.setViewControllers([first], animated: true) + } + }) + return true + } + }) + } + } + } + + open override func tableWillBind(_ binder: AABinder) { + + binder.bind(self.group.shortName) { (value: String?) in + if let row = self.shortNameRow { + row.reload() + } + } + + binder.bind(self.group.isHistoryShared) { (value: JavaLangBoolean?) in + if let row = self.shareHistoryRow { + row.reload() + } + } + } +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAGroupEditInfoViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAGroupEditInfoViewController.swift new file mode 100644 index 0000000000..791f0b10a4 --- /dev/null +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAGroupEditInfoViewController.swift @@ -0,0 +1,166 @@ +// +// Copyright (c) 2014-2016 Actor LLC. +// + +import Foundation +import SZTextView + +open class AAGroupEditInfoController: AAViewController, UITextViewDelegate { + + fileprivate var isChannel = false + fileprivate let scrollView = UIScrollView() + fileprivate let bgContainer = UIView() + fileprivate let topSeparator = UIView() + fileprivate let bottomSeparator = UIView() + + fileprivate let avatarView = AAAvatarView() + fileprivate let nameInput = UITextField() + fileprivate let nameInputSeparator = UIView() + fileprivate let descriptionView = SZTextView() + fileprivate let descriptionSeparator = UIView() + + public init(gid: Int) { + super.init() + self.gid = gid + self.isChannel = group.groupType == ACGroupType.channel() + } + + public required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = appStyle.vcBackyardColor + + scrollView.alwaysBounceVertical = true + + nameInputSeparator.backgroundColor = appStyle.vcSeparatorColor + topSeparator.backgroundColor = appStyle.vcSeparatorColor + bottomSeparator.backgroundColor = appStyle.vcSeparatorColor + descriptionSeparator.backgroundColor = appStyle.vcSeparatorColor + + bgContainer.backgroundColor = appStyle.vcBgColor + + scrollView.addSubview(bgContainer) + bgContainer.addSubview(avatarView) + bgContainer.addSubview(nameInput) + bgContainer.addSubview(nameInputSeparator) + bgContainer.addSubview(descriptionView) + bgContainer.addSubview(descriptionSeparator) + bgContainer.addSubview(topSeparator) + bgContainer.addSubview(bottomSeparator) + view.addSubview(scrollView) + + avatarView.bind(group.name.get(), id: gid, avatar: group.avatar.get()) + avatarView.viewDidTap = { + self.avatarDidTap() + } + + nameInput.font = UIFont.systemFont(ofSize: 19) + if isChannel { + nameInput.placeholder = AALocalized("GroupEditNameChannel") + } else { + nameInput.placeholder = AALocalized("GroupEditName") + } + nameInput.text = group.name.get() + + descriptionView.delegate = self + descriptionView.font = UIFont.systemFont(ofSize: 17) + descriptionView.placeholder = AALocalized("GroupEditDescription") + descriptionView.text = group.about.get() + descriptionView.isScrollEnabled = false + + if isChannel { + navigationItem.title = AALocalized("GroupEditTitleChannel") + } else { + navigationItem.title = AALocalized("GroupEditTitle") + } + navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationSave"), style: .done, target: self, action: #selector(saveDidPressed)) + navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: .done, target: self, action: #selector(cancelEdit)) + } + + open override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + + scrollView.frame = CGRect(x: 0, y: 0, width: view.width, height: view.height) + + nameInput.frame = CGRect(x: 94, y: 34, width: view.width - 94 - 10, height: 24) + nameInputSeparator.frame = CGRect(x: 94, y: 34 + 24, width: view.width - 94, height: 0.5) + avatarView.frame = CGRect(x: 14, y: 14, width: 66, height: 66) + + descriptionSeparator.frame = CGRect(x: 0, y: 94, width: view.width, height: 0.5) + topSeparator.frame = CGRect(x: 0, y: 0, width: view.width, height: 0.5) + + layoutContainer() + } + + open func avatarDidTap() { + let hasCamera = UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera) + self.showActionSheet( hasCamera ? ["PhotoCamera", "PhotoLibrary"] : ["PhotoLibrary"], + cancelButton: "AlertCancel", + destructButton: self.group.getAvatarModel().get() != nil ? "PhotoRemove" : nil, + sourceView: self.view, + sourceRect: self.view.bounds, + tapClosure: { (index) -> () in + if (index == -2) { + self.confirmAlertUser("PhotoRemoveGroupMessage", + action: "PhotoRemove", + tapYes: { () -> () in + Actor.removeGroupAvatar(withGid: jint(self.gid)) + self.avatarView.bind(self.group.name.get(), id: self.gid, avatar: nil) + }, tapNo: nil) + } else if (index >= 0) { + let takePhoto: Bool = (index == 0) && hasCamera + self.pickAvatar(takePhoto, closure: { (image) -> () in + let fileName = Actor.changeGroupAvatar(jint(self.gid), image: image) + self.avatarView.bind(self.group.name.get(), id: self.gid, fileName: CocoaFiles.pathFromDescriptor(fileName)) + }) + } + }) + } + + open func saveDidPressed() { + let text = nameInput.text!.trim() + let about = self.descriptionView.text!.trim() + nameInput.resignFirstResponder() + descriptionView.resignFirstResponder() + if text != group.name.get() { + executePromise(Actor.editGroupTitle(withGid: jint(gid), withTitle: text).then({ (v: ARVoid!) in + if about != self.group.about.get() { + self.executePromise(Actor.editGroupAbout(withGid: jint(self.gid), withAbout: about).then({ (v: ARVoid!) in + self.cancelEdit() + })) + } else { + self.cancelEdit() + } + })) + } else { + if about != self.group.about.get() { + self.executePromise(Actor.editGroupAbout(withGid: jint(self.gid), withAbout: about).then({ (v: ARVoid!) in + self.cancelEdit() + })) + } else { + self.cancelEdit() + } + } + } + + func cancelEdit() { + nameInput.resignFirstResponder() + descriptionView.resignFirstResponder() + dismissController() + } + + open func textViewDidChange(_ textView: UITextView) { + layoutContainer() + } + + fileprivate func layoutContainer() { + let newSize = descriptionView.sizeThatFits(CGSize(width: view.width - 20, height: CGFloat.greatestFiniteMagnitude)) + descriptionView.frame = CGRect(x: 10, y: 102, width: view.width - 20, height: max(newSize.height, 33)) + bgContainer.frame = CGRect(x: 0, y: 0, width: view.width, height: 100 + descriptionView.height + 8) + bottomSeparator.frame = CGRect(x: 0, y: bgContainer.height - 0.5, width: bgContainer.width, height: 0.5) + } +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAGroupTypeController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAGroupTypeController.swift new file mode 100644 index 0000000000..2b8a7285f9 --- /dev/null +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAGroupTypeController.swift @@ -0,0 +1,184 @@ +// +// Copyright (c) 2014-2016 Actor LLC. +// + +import Foundation + +open class AAGroupTypeViewController: AAContentTableController { + + fileprivate let isCreation: Bool + fileprivate var isChannel: Bool = false + fileprivate var isPublic: Bool = false + fileprivate var linkSection: AAManagedSection! + fileprivate var publicRow: AACommonRow! + fileprivate var privateRow: AACommonRow! + fileprivate var shortNameRow: AAEditRow! + + public init(gid: Int, isCreation: Bool) { + self.isCreation = isCreation + super.init(style: .settingsGrouped) + self.gid = gid + self.isChannel = group.groupType == ACGroupType.channel() + if (isChannel) { + navigationItem.title = AALocalized("GroupTypeTitleChannel") + } else { + navigationItem.title = AALocalized("GroupTypeTitle") + } + if isCreation { + navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationNext"), style: .done, target: self, action: #selector(saveDidTap)) + } else { + navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationSave"), style: .done, target: self, action: #selector(saveDidTap)) + } + } + + public required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open override func tableDidLoad() { + + self.isPublic = group.shortName.get() != nil + + section { (s) in + + if isChannel { + s.headerText = AALocalized("GroupTypeTitleChannel").uppercased() + if self.isPublic { + s.footerText = AALocalized("GroupTypeHintPublicChannel") + } else { + s.footerText = AALocalized("GroupTypeHintPrivateChannel") + } + } else { + s.headerText = AALocalized("GroupTypeTitle").uppercased() + if self.isPublic { + s.footerText = AALocalized("GroupTypeHintPublic") + } else { + s.footerText = AALocalized("GroupTypeHintPrivate") + } + } + self.publicRow = s.common({ (r) in + if isChannel { + r.content = AALocalized("ChannelTypePublicFull") + } else { + r.content = AALocalized("GroupTypePublicFull") + } + + r.selectAction = { () -> Bool in + if !self.isPublic { + self.isPublic = true + self.publicRow.rebind() + self.privateRow.rebind() + if self.isChannel { + s.footerText = AALocalized("GroupTypeHintPublicChannel") + } else { + s.footerText = AALocalized("GroupTypeHintPublic") + } + self.tableView.reloadSection(0, with: .automatic) + self.managedTable.sections.append(self.linkSection) + self.tableView.insertSection(1, with: .fade) + } + return true + } + r.bindAction = { (r) in + if self.isPublic { + r.style = .checkmark + } else { + r.style = .normal + } + } + }) + + self.privateRow = s.common({ (r) in + if isChannel { + r.content = AALocalized("ChannelTypePrivateFull") + } else { + r.content = AALocalized("GroupTypePrivateFull") + } + + r.selectAction = { () -> Bool in + if self.isPublic { + self.isPublic = false + self.publicRow.rebind() + self.privateRow.rebind() + if self.isChannel { + s.footerText = AALocalized("GroupTypeHintPrivateChannel") + } else { + s.footerText = AALocalized("GroupTypeHintPrivate") + } + self.tableView.reloadSection(0, with: .automatic) + self.managedTable.sections.remove(at: 1) + self.tableView.deleteSection(1, with: .fade) + } + return true + } + r.bindAction = { (r) in + if !self.isPublic { + r.style = .checkmark + } else { + r.style = .normal + } + } + }) + } + + self.linkSection = section { (s) in + if self.isChannel { + s.footerText = AALocalized("GroupTypeLinkHintChannel") + } else { + s.footerText = AALocalized("GroupTypeLinkHint") + } + + self.shortNameRow = s.edit({ (r) in + r.autocapitalizationType = .none + r.prefix = ActorSDK.sharedActor().invitePrefixShort + r.text = self.group.shortName.get() + }) + } + if !self.isPublic { + managedTable.sections.remove(at: 1) + } + } + + open func saveDidTap() { + let nShortName: String? + if self.isPublic { + if let shortNameVal = self.shortNameRow.text?.trim() { + if shortNameVal.length > 0 { + nShortName = shortNameVal + } else { + nShortName = nil + } + } else { + nShortName = nil + } + } else { + nShortName = nil + } + + if nShortName != group.shortName.get() { + executePromise(Actor.editGroupShortName(withGid: jint(self.gid), withAbout: nShortName).then({ (r:ARVoid!) in + if (self.isCreation) { + if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer.group(with: jint(self.gid))) { + self.navigateDetail(customController) + } else { + self.navigateDetail(ConversationViewController(peer: ACPeer.group(with: jint(self.gid)))) + } + self.dismissController() + } else { + self.navigateBack() + } + })) + } else { + if (isCreation) { + if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer.group(with: jint(self.gid))) { + self.navigateDetail(customController) + } else { + self.navigateDetail(ConversationViewController(peer: ACPeer.group(with: jint(self.gid)))) + } + self.dismissController() + } else { + navigateBack() + } + } + } +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAGroupViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAGroupViewController.swift index a3fa1c7b5a..2e1be90ce5 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAGroupViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAGroupViewController.swift @@ -4,22 +4,23 @@ import UIKit -public class AAGroupViewController: AAContentTableController { +open class AAGroupViewController: AAContentTableController { - private let membersSort = { (left: ACGroupMember, right: ACGroupMember) -> Bool in + fileprivate let membersSort = { (left: ACGroupMember, right: ACGroupMember) -> Bool in let lname = Actor.getUserWithUid(left.uid).getNameModel().get() let rname = Actor.getUserWithUid(right.uid).getNameModel().get() - return lname < rname + return lname! < rname! } - public var headerRow: AAAvatarRow! - public var memberRows: AAManagedArrayRows! + open var headerRow: AAAvatarRow! + open var memberRows: AAManagedArrayRows! public init (gid: Int) { - super.init(style: AAContentTableStyle.SettingsPlain) + super.init(style: AAContentTableStyle.settingsPlain) self.gid = gid self.autoTrack = true + self.unbindOnDissapear = true self.title = AALocalized("ProfileTitle") } @@ -29,312 +30,315 @@ public class AAGroupViewController: AAContentTableController { } - public override func tableDidLoad() { + open override func tableDidLoad() { + + // NavigationBar + if self.group.isCanEditInfo.get().booleanValue() { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationEdit"), style: .plain, target: self, action: #selector(editDidPressed)) + } else { + self.navigationItem.rightBarButtonItem = nil + } // Header section { (s) -> () in - - // Header: Avatar self.headerRow = s.avatar { (r) -> () in r.id = self.gid - r.subtitleHidden = true r.bindAction = { (r) -> () in r.avatar = self.group.getAvatarModel().get() r.title = self.group.getNameModel().get() + r.subtitle = Actor.getFormatter().formatGroupMembers(self.group.getMembersCountModel().get().intValue()) } r.avatarDidTap = { (view) -> () in let avatar = self.group.getAvatarModel().get() - if avatar != nil && avatar.fullImage != nil { + if avatar != nil && avatar?.fullImage != nil { - let full = avatar.fullImage.fileReference - let small = avatar.smallImage.fileReference - let size = CGSize(width: Int(avatar.fullImage.width), height: Int(avatar.fullImage.height)) + let full = avatar?.fullImage.fileReference + let small = avatar?.smallImage.fileReference + let size = CGSize(width: Int((avatar?.fullImage.width)!), height: Int((avatar?.fullImage.height)!)) - self.presentViewController(AAPhotoPreviewController(file: full, previewFile: small, size: size, fromView: view), animated: true, completion: nil) + self.present(AAPhotoPreviewController(file: full!, previewFile: small, size: size, fromView: view), animated: true, completion: nil) } } } - - // Header: Change photo - s.action("GroupSetPhoto") { (r) -> () in - r.selectAction = { () -> Bool in - let hasCamera = UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.Camera) - self.showActionSheet( hasCamera ? ["PhotoCamera", "PhotoLibrary"] : ["PhotoLibrary"], - cancelButton: "AlertCancel", - destructButton: self.group.getAvatarModel().get() != nil ? "PhotoRemove" : nil, - sourceView: self.view, - sourceRect: self.view.bounds, - tapClosure: { (index) -> () in - if (index == -2) { - self.confirmAlertUser("PhotoRemoveGroupMessage", - action: "PhotoRemove", - tapYes: { () -> () in - Actor.removeGroupAvatarWithGid(jint(self.gid)) - }, tapNo: nil) - } else if (index >= 0) { - let takePhoto: Bool = (index == 0) && hasCamera - self.pickAvatar(takePhoto, closure: { (image) -> () in - Actor.changeGroupAvatar(jint(self.gid), image: image) - }) - } - }) - - return true - } + } + + // About + if let about = self.group.about.get() { + section { (s) in + + s.autoSeparatorTopOffset = 1 + + s.header(AALocalized("GroupDescription").uppercased()).height = 22 + + s.text(nil, content: about) } - - // Header: Change title - s.action("GroupSetTitle") { (r) -> () in - r.selectAction = { () -> Bool in - self.startEditField { (c) -> () in - - c.title = "GroupEditHeader" - - c.fieldHint = "GroupEditHint" - - c.actionTitle = "NavigationSave" - - c.initialText = self.group.getNameModel().get() - - c.didDoneTap = { (t, c) -> () in - - if t.length == 0 { - return - } - - c.executeSafeOnlySuccess(Actor.editGroupTitleCommandWithGid(jint(self.gid), withTitle: t)!, successBlock: { (val) -> Void in - c.dismiss() - }) - } - } - - return true - } + } + + // Link + if let shortName = self.group.shortName.get(), let prefix = ActorSDK.sharedActor().invitePrefix { + section { (s) in + s.text(nil, content: prefix + shortName) } - } // Calls if (ActorSDK.sharedActor().enableCalls) { let members = (group.members.get() as! JavaUtilHashSet).size() if (members <= 20) { // Temporary limitation - section { (s) -> () in - s.action("CallsStartGroupAudio") { (r) -> () in - r.selectAction = { () -> Bool in - self.execute(Actor.doCallWithGid(jint(self.gid))) - return true + if group.groupType == ACGroupType.group() { + section { (s) -> () in + s.action("CallsStartGroupAudio") { (r) -> () in + r.selectAction = { () -> Bool in + self.execute(Actor.doCall(withGid: jint(self.gid))) + return true + } } } } } } - // Notifications + // Actions and Settings section { (s) -> () in - let groupPeer: ACPeer! = ACPeer.groupWithInt(jint(self.gid)) - + // Notifications s.common { (r) -> () in - r.style = .Switch + let groupPeer: ACPeer! = ACPeer.group(with: jint(self.gid)) + + r.style = .switch r.content = AALocalized("GroupNotifications") r.bindAction = { (r) -> () in - r.switchOn = Actor.isNotificationsEnabledWithPeer(groupPeer) + r.switchOn = Actor.isNotificationsEnabled(with: groupPeer) } r.switchAction = { (on: Bool) -> () in - Actor.changeNotificationsEnabledWithPeer(groupPeer, withValue: on) + Actor.changeNotificationsEnabled(with: groupPeer, withValue: on) } if(ActorSDK.sharedActor().enableChatGroupSound) { - if(Actor.isNotificationsEnabledWithPeer(groupPeer)){ + if(Actor.isNotificationsEnabled(with: groupPeer)){ r.selectAction = {() -> Bool in // Sound: Choose sound let setRingtoneController = AARingtonesViewController() - let soundForGroupe = Actor.getNotificationsSoundWithPeer(groupPeer) - setRingtoneController.selectedRingtone = (soundForGroupe != nil) ? soundForGroupe : "" + let soundForGroupe = Actor.getNotificationsSound(with: groupPeer) + setRingtoneController.selectedRingtone = (soundForGroupe != nil) ? soundForGroupe! : "" setRingtoneController.completion = {(selectedSound:String) in - Actor.changeNotificationsSoundPeer(groupPeer, withValue: selectedSound) + Actor.changeNotificationsSound(groupPeer, withValue: selectedSound) } let navigationController = AANavigationController(rootViewController: setRingtoneController) if (AADevice.isiPad) { - navigationController.modalInPopover = true - navigationController.modalPresentationStyle = UIModalPresentationStyle.CurrentContext + navigationController.isModalInPopover = true + navigationController.modalPresentationStyle = UIModalPresentationStyle.currentContext } - self.presentViewController(navigationController, animated: true, completion: { - } - ) + self.present(navigationController, animated: true, completion: nil) return false } } } } - } - - // Members - section { (s) -> () in - s.autoSeparatorsInset = 65 - s.autoSeparatorTopOffset = 1 - s.headerHeight = 0 - - // Members: Header - s.header(AALocalized("GroupMembers").uppercaseString) + // Admininstration + if group.isCanEditAdministration.get().booleanValue() || group.isCanDelete.get().booleanValue() { + s.common({ (r) in + r.content = AALocalized("GroupAdministration") + r.selectAction = { () -> Bool in + self.navigateNext(AAGroupAdministrationViewController(gid: self.gid)) + return false + } + }) + } - // Members: Add - s.action("GroupAddParticipant") { (r) -> () in - - r.contentInset = 65 + // View Members + if self.group.isCanViewMembers.get().booleanValue() && self.group.isAsyncMembers.get().booleanValue() { + s.common({ (r) -> () in + r.content = AALocalized("GroupViewMembers") + r.style = .normal + r.selectAction = { () -> Bool in + self.navigateNext(AAGroupViewMembersController(gid: self.gid)) + return false + } + }) - r.selectAction = { () -> Bool in - let addParticipantController = AAAddParticipantViewController(gid: self.gid) - let navigationController = AANavigationController(rootViewController: addParticipantController) - if (AADevice.isiPad) { - navigationController.modalInPopover = true - navigationController.modalPresentationStyle = UIModalPresentationStyle.CurrentContext + s.action("GroupAddParticipant") { (r) -> () in + + r.selectAction = { () -> Bool in + let addParticipantController = AAAddParticipantViewController(gid: self.gid) + let navigationController = AANavigationController(rootViewController: addParticipantController) + if (AADevice.isiPad) { + navigationController.isModalInPopover = true + navigationController.modalPresentationStyle = UIModalPresentationStyle.currentContext + } + self.present(navigationController, animated: true, completion: nil) + return false + } - self.presentViewController(navigationController, animated: true, completion: nil) - return false } } + + } + + // Members + + if group.isCanViewMembers.get().booleanValue() && !group.isAsyncMembers.get().booleanValue() { - // Members: List - self.memberRows = s.arrays { (r: AAManagedArrayRows) -> () in - r.height = 48 - r.data = self.group.members.get().toArray().toSwiftArray() - r.data.sortInPlace(self.membersSort) + section { (s) -> () in - r.bindData = { (c, d) -> () in - let user = Actor.getUserWithUid(d.uid) - c.bind(user, isAdmin: d.isAdministrator) - - // Notify to request onlines - Actor.onUserVisibleWithUid(d.uid) - } + s.autoSeparatorsInset = 65 + s.autoSeparatorTopOffset = 1 + s.headerHeight = 0 - r.selectAction = { (d) -> Bool in - let user = Actor.getUserWithUid(d.uid) - if (user.getId() == Actor.myUid()) { - return true - } + // Members: Header + s.header(AALocalized("GroupViewMembers").uppercased()) + + // Members: Add + s.action("GroupAddParticipant") { (r) -> () in - let name = user.getNameModel().get() + r.contentInset = 65 - self.alertSheet { (a: AAAlertSetting) -> () in - - a.cancel = "AlertCancel" - - a.action("GroupMemberInfo") { () -> () in - var controller: AAViewController! = ActorSDK.sharedActor().delegate.actorControllerForUser(Int(user.getId())) - if controller == nil { - controller = AAUserViewController(uid: Int(user.getId())) - } - self.navigateNext(controller, removeCurrent: false) + r.selectAction = { () -> Bool in + let addParticipantController = AAAddParticipantViewController(gid: self.gid) + let navigationController = AANavigationController(rootViewController: addParticipantController) + if (AADevice.isiPad) { + navigationController.isModalInPopover = true + navigationController.modalPresentationStyle = UIModalPresentationStyle.currentContext } + self.present(navigationController, animated: true, completion: nil) + return false + } + } + + // Members: List + self.memberRows = s.arrays { (r: AAManagedArrayRows) -> () in + r.height = 48 + r.data = (self.group.members.get() as AnyObject).toArray().toSwiftArray() + r.data.sort(by: self.membersSort) + + r.bindData = { (c, d) -> () in + let user = Actor.getUserWithUid(d.uid) + c.bind(user, isAdmin: d.isAdministrator) - a.action("GroupMemberWrite") { () -> () in - if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer.userWithInt(user.getId())) { - self.navigateDetail(customController) - } else { - self.navigateDetail(ConversationViewController(peer: ACPeer.userWithInt(user.getId()))) - } - self.popover?.dismissPopoverAnimated(true) + // Notify to request onlines + Actor.onUserVisible(withUid: d.uid) + } + + r.selectAction = { (d) -> Bool in + let user = Actor.getUserWithUid(d.uid) + if (user.getId() == Actor.myUid()) { + return true } - a.action("GroupMemberCall", closure: { () -> () in - let phones = user.getPhonesModel().get() - if phones.size() == 0 { - self.alertUser("GroupMemberCallNoPhones") - } else if phones.size() == 1 { - let number = phones.getWithInt(0) - ActorSDK.sharedActor().openUrl("telprompt://+\(number.phone)") - } else { - - var numbers = [String]() - for i in 0.. () in + + a.cancel = "AlertCancel" + + a.action("GroupMemberInfo") { () -> () in + var controller: AAViewController! = ActorSDK.sharedActor().delegate.actorControllerForUser(Int(user.getId())) + if controller == nil { + controller = AAUserViewController(uid: Int(user.getId())) } - self.showActionSheet(numbers, - cancelButton: "AlertCancel", - destructButton: nil, - sourceView: self.view, - sourceRect: self.view.bounds, - tapClosure: { (index) -> () in - if (index >= 0) { - let number = phones.getWithInt(jint(index)) - ActorSDK.sharedActor().openUrl("telprompt://+\(number.phone)") - } - }) + self.navigateNext(controller, removeCurrent: false) } - }) - - // Detect if we are admin - let members: [ACGroupMember] = self.group.members.get().toArray().toSwiftArray() - var isAdmin = self.group.creatorId == Actor.myUid() - if !isAdmin { - for m in members { - if m.uid == Actor.myUid() { - isAdmin = m.isAdministrator + + a.action("GroupMemberWrite") { () -> () in + if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer.user(with: user.getId())) { + self.navigateDetail(customController) + } else { + self.navigateDetail(ConversationViewController(peer: ACPeer.user(with: user.getId()))) } + self.popover?.dismiss(animated: true) } - } - - // Can mark as admin - let canMarkAdmin = isAdmin && !d.isAdministrator - - if canMarkAdmin { - a.action("GroupMemberMakeAdmin") { () -> () in - - self.confirmDestructive(AALocalized("GroupMemberMakeMessage").replace("{name}", dest: name), action: AALocalized("GroupMemberMakeAction")) { + + a.action("GroupMemberCall", closure: { () -> () in + let phones = user.getPhonesModel().get() + if phones?.size() == 0 { + self.alertUser("GroupMemberCallNoPhones") + } else if phones?.size() == 1 { + let number = phones!.getWith(0) + ActorSDK.sharedActor().openUrl("telprompt://+\(number!.phone)") + } else { - self.executeSafe(Actor.makeAdminCommandWithGid(jint(self.gid), withUid: jint(user.getId()))!) + var numbers = [String]() + for i in 0.. () in + if (index >= 0) { + let number = phones!.getWith(jint(index)) + ActorSDK.sharedActor().openUrl("telprompt://+\(number!.phone)") + } + }) } - } - } - - // Can kick user - let canKick = isAdmin || d.inviterUid == Actor.myUid() - - if canKick { - a.destructive("GroupMemberKick") { () -> () in - self.confirmDestructive(AALocalized("GroupMemberKickMessage") - .replace("{name}", dest: name), action: AALocalized("GroupMemberKickAction")) { - - self.executeSafe(Actor.kickMemberCommandWithGid(jint(self.gid), withUid: user.getId())!) + }) + + // Can kick user + let canKick: Bool = + (self.group.isCanKickAnyone.get().booleanValue() || + (self.group.isCanKickInvited.get().booleanValue() && d.inviterUid == Actor.myUid())) + + if canKick { + let name = Actor.getUserWithUid(d.uid).getNameModel().get() + a.destructive("GroupMemberKick") { () -> () in + self.confirmDestructive(AALocalized("GroupMemberKickMessage") + .replace("{name}", dest: name!), action: AALocalized("GroupMemberKickAction")) { + self.executeSafe(Actor.kickMemberCommand(withGid: jint(self.gid), withUid: user.getId())) + } } } } + + return true } - - return true } } } // Leave group - section { (s) -> () in - s.common({ (r) -> () in - r.content = AALocalized("GroupLeave") - r.style = .DestructiveCentered - r.selectAction = { () -> Bool in + if group.isCanLeave.get().booleanValue() { + section { (s) -> () in + s.common({ (r) -> () in - self.confirmDestructive(AALocalized("GroupLeaveConfirm"), action: AALocalized("GroupLeaveConfirmAction"), yes: { () -> () in - self.executeSafe(Actor.leaveGroupCommandWithGid(jint(self.gid))!) - }) + if self.group.groupType == ACGroupType.channel() { + r.content = AALocalized("ActionLeaveChannel") + } else { + r.content = AALocalized("ActionDeleteAndExit") + } - return true - } - }) + r.style = .destructive + r.selectAction = { () -> Bool in + + let title: String + let action: String + if self.group.groupType == ACGroupType.channel() { + title = AALocalized("ActionLeaveChannelMessage") + action = AALocalized("ActionLeaveChannelAction") + } else { + title = AALocalized("ActionDeleteAndExitMessage") + action = AALocalized("ActionDeleteAndExitMessageAction") + } + self.confirmDestructive(title, action: action, yes: { () -> () in + self.executePromise(Actor.leaveAndDeleteGroup(withGid: jint(self.gid))) + }) + + return true + } + }) + } } } - public override func tableWillBind(binder: AABinder) { + open override func tableWillBind(_ binder: AABinder) { // Bind group info @@ -347,12 +351,13 @@ public class AAGroupViewController: AAContentTableController { } // Bind members - - binder.bind(group.getMembersModel()) { (value: JavaUtilHashSet?) -> () in - if let v = value { - self.memberRows.data = v.toArray().toSwiftArray() - self.memberRows.data.sortInPlace(self.membersSort) - self.memberRows.reload() + if memberRows != nil{ + binder.bind(group.getMembersModel()) { (value: JavaUtilHashSet?) -> () in + if let v = value { + self.memberRows.data = v.toArray().toSwiftArray() + self.memberRows.data.sort(by: self.membersSort) + self.memberRows.reload() + } } } @@ -373,4 +378,8 @@ public class AAGroupViewController: AAContentTableController { } } } -} \ No newline at end of file + + open func editDidPressed() { + self.presentInNavigation(AAGroupEditInfoController(gid: gid)) + } +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAGroupViewMembersController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAGroupViewMembersController.swift new file mode 100644 index 0000000000..a2499c9217 --- /dev/null +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/AAGroupViewMembersController.swift @@ -0,0 +1,154 @@ +// +// Copyright (c) 2014-2016 Actor LLC. +// + +import Foundation + +open class AAGroupViewMembersController: AAContentTableController { + + fileprivate var membersRow: AAManagedArrayRows! + + fileprivate var isLoaded = false + fileprivate var isLoading = false + open var nextBatch: IOSByteArray! = nil + + public init(gid: Int) { + super.init(style: .plain) + self.gid = gid + + navigationItem.title = AALocalized("GroupViewMembers") + + if group.isCanInviteMembers.get().booleanValue() { + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.add, target: self, action: #selector(didAddPressed)) + } + } + + public required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open override func tableDidLoad() { + section { (s) in + self.membersRow = s.arrays({ (r: AAManagedArrayRows) -> () in + r.height = 48 + r.data = [ACGroupMember]() + + r.bindData = { (c, d) -> () in + let user = Actor.getUserWithUid(d.uid) + c.bind(user, isAdmin: d.isAdministrator) + + // Notify to request onlines + Actor.onUserVisible(withUid: d.uid) + } + + r.itemShown = { (index, d) in + if index > r.data.count - 10 { + self.loadMore() + } + } + + r.selectAction = { (d) -> Bool in + let user = Actor.getUserWithUid(d.uid) + if (user.getId() == Actor.myUid()) { + return true + } + + self.alertSheet { (a: AAAlertSetting) -> () in + + a.cancel = "AlertCancel" + + a.action("GroupMemberInfo") { () -> () in + var controller: AAViewController! = ActorSDK.sharedActor().delegate.actorControllerForUser(Int(user.getId())) + if controller == nil { + controller = AAUserViewController(uid: Int(user.getId())) + } + self.navigateNext(controller, removeCurrent: false) + } + + a.action("GroupMemberWrite") { () -> () in + if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer.user(with: user.getId())) { + self.navigateDetail(customController) + } else { + self.navigateDetail(ConversationViewController(peer: ACPeer.user(with: user.getId()))) + } + self.popover?.dismiss(animated: true) + } + + a.action("GroupMemberCall", closure: { () -> () in + let phones = user.getPhonesModel().get()! + if phones.size() == 0 { + self.alertUser("GroupMemberCallNoPhones") + } else if phones.size() == 1 { + let number = phones.getWith(0)! + ActorSDK.sharedActor().openUrl("telprompt://+\(number.phone)") + } else { + + var numbers = [String]() + for i in 0.. () in + if (index >= 0) { + let number = phones.getWith(jint(index))! + ActorSDK.sharedActor().openUrl("telprompt://+\(number.phone)") + } + }) + } + }) + + // Can kick user + let canKick: Bool = + (self.group.isCanKickAnyone.get().booleanValue() || + (self.group.isCanKickInvited.get().booleanValue() && d.inviterUid == Actor.myUid())) + + if canKick { + let name = Actor.getUserWithUid(d.uid).getNameModel().get() + a.destructive("GroupMemberKick") { () -> () in + self.confirmDestructive(AALocalized("GroupMemberKickMessage") + .replace("{name}", dest: name!), action: AALocalized("GroupMemberKickAction")) { + self.executeSafe(Actor.kickMemberCommand(withGid: jint(self.gid), withUid: user.getId())) + } + } + } + } + + return true + } + }) + } + + loadMore() + } + + fileprivate func loadMore() { + if isLoading { + return + } + if isLoaded { + return + } + + isLoading = true + Actor.loadMembers(withGid: jint(gid), withLimit: 20, withNext: nextBatch).then { (slice: ACGroupMembersSlice!) in + for i in 0.. () in s.headerText = AALocalized("GroupInviteLinkTitle") @@ -44,7 +44,7 @@ public class AAInviteLinkViewController: AAContentTableController { section { (s) -> () in s.action("ActionCopyLink") { (r) -> () in r.selectAction = { () -> Bool in - UIPasteboard.generalPasteboard().string = self.currentUrl + UIPasteboard.general.string = self.currentUrl self.alertUser("AlertLinkCopied") return true } @@ -52,9 +52,9 @@ public class AAInviteLinkViewController: AAContentTableController { s.action("ActionShareLink") { (r) -> () in r.selectAction = { () -> Bool in var sharingItems = [AnyObject]() - sharingItems.append(self.currentUrl!) + sharingItems.append(self.currentUrl! as AnyObject) let activityViewController = UIActivityViewController(activityItems: sharingItems, applicationActivities: nil) - self.presentViewController(activityViewController, animated: true, completion: nil) + self.present(activityViewController, animated: true, completion: nil) return true } } @@ -71,18 +71,18 @@ public class AAInviteLinkViewController: AAContentTableController { } } - executeSafe(Actor.requestInviteLinkCommandWithGid(jint(gid))!) { (val) -> Void in + executeSafe(Actor.requestInviteLinkCommand(withGid: jint(gid))) { (val) -> Void in self.currentUrl = val as? String self.urlRow.reload() - self.tableView.hidden = false + self.tableView.isHidden = false } } - public func reloadLink() { - executeSafe(Actor.requestRevokeLinkCommandWithGid(jint(gid))!) { (val) -> Void in + open func reloadLink() { + executeSafe(Actor.requestRevokeLinkCommand(withGid: jint(gid))) { (val) -> Void in self.currentUrl = val as? String self.urlRow.reload() - self.tableView.hidden = false + self.tableView.isHidden = false } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/Cells/AAGroupMemberCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/Cells/AAGroupMemberCell.swift index 5ff50f7042..3b47e0c6cc 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/Cells/AAGroupMemberCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Group/Cells/AAGroupMemberCell.swift @@ -4,18 +4,18 @@ import UIKit -public class AAGroupMemberCell: AATableViewCell { +open class AAGroupMemberCell: AATableViewCell { // Views - public var nameLabel = UILabel() - public var onlineLabel = UILabel() - public var avatarView = AAAvatarView() - public var adminLabel = UILabel() + open var nameLabel = UILabel() + open var onlineLabel = UILabel() + open var avatarView = AAAvatarView() + open var adminLabel = UILabel() // Binder - public var binder = AABinder() + open var binder = AABinder() // Contstructors @@ -27,14 +27,14 @@ public class AAGroupMemberCell: AATableViewCell { contentView.addSubview(avatarView) - nameLabel.font = UIFont.systemFontOfSize(18.0) + nameLabel.font = UIFont.systemFont(ofSize: 18.0) nameLabel.textColor = appStyle.cellTextColor contentView.addSubview(nameLabel) - onlineLabel.font = UIFont.systemFontOfSize(14.0) + onlineLabel.font = UIFont.systemFont(ofSize: 14.0) contentView.addSubview(onlineLabel) - adminLabel.font = UIFont.systemFontOfSize(14.0) + adminLabel.font = UIFont.systemFont(ofSize: 14.0) adminLabel.textColor = appStyle.cellDestructiveColor contentView.addSubview(adminLabel) } @@ -45,19 +45,19 @@ public class AAGroupMemberCell: AATableViewCell { // Binding - public func setUsername(username: String) { + open func setUsername(_ username: String) { nameLabel.text = username } - public func bind(user: ACUserVM, isAdmin: Bool) { + open func bind(_ user: ACUserVM, isAdmin: Bool) { // Bind name and avatar - let name = user.getNameModel().get() + let name = user.getNameModel().get()! nameLabel.text = name avatarView.bind(name, id: Int(user.getId()), avatar: user.getAvatarModel().get()) // Bind admin flag - adminLabel.hidden = !isAdmin + adminLabel.isHidden = !isAdmin // Bind onlines @@ -70,8 +70,8 @@ public class AAGroupMemberCell: AATableViewCell { if value != nil { self.onlineLabel.showView() - self.onlineLabel.text = Actor.getFormatter().formatPresence(value!, withSex: user.getSex()) - if value!.state.ordinal() == ACUserPresence_State.ONLINE().ordinal() { + self.onlineLabel.text = Actor.getFormatter().formatPresence(value!, with: user.getSex()) + if value!.state.ordinal() == ACUserPresence_State.online().ordinal() { self.onlineLabel.textColor = self.appStyle.userOnlineColor } else { self.onlineLabel.textColor = self.appStyle.userOfflineColor @@ -84,14 +84,14 @@ public class AAGroupMemberCell: AATableViewCell { } } - public override func prepareForReuse() { + open override func prepareForReuse() { super.prepareForReuse() binder.unbindAll() } // Layouting - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() let userAvatarViewFrameSize: CGFloat = CGFloat(44) @@ -99,7 +99,7 @@ public class AAGroupMemberCell: AATableViewCell { var w: CGFloat = contentView.bounds.size.width - 65.0 - 8.0 - if !adminLabel.hidden { + if !adminLabel.isHidden { adminLabel.frame = CGRect(x: contentView.width - adminLabel.width - 8, y: 5, width: adminLabel.width, height: 42) w -= adminLabel.width + 8 } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Location/AALocationPickerController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Location/AALocationPickerController.swift index 4e402d339f..e35f5260ce 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Location/AALocationPickerController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Location/AALocationPickerController.swift @@ -5,20 +5,20 @@ import Foundation import MapKit -public class AALocationPickerController: AAViewController, CLLocationManagerDelegate, MKMapViewDelegate { +open class AALocationPickerController: AAViewController, CLLocationManagerDelegate, MKMapViewDelegate { - public var delegate: AALocationPickerControllerDelegate? = nil + open var delegate: AALocationPickerControllerDelegate? = nil - private let locationManager = CLLocationManager() - private let map = MKMapView() - private let pinPoint = AAMapPinPointView() - private let locationPinOffset = CGPointMake(0.0, 33.0) + fileprivate let locationManager = CLLocationManager() + fileprivate let map = MKMapView() + fileprivate let pinPoint = AAMapPinPointView() + fileprivate let locationPinOffset = CGPoint(x: 0.0, y: 33.0) override init() { super.init() navigationItem.title = AALocalized("LocationTitle") - navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: .Plain, target: self, action: #selector(AALocationPickerController.cancellDidTap)) + navigationItem.leftBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: .plain, target: self, action: #selector(AALocationPickerController.cancellDidTap)) updateAuthStatus(CLLocationManager.authorizationStatus()) @@ -46,11 +46,11 @@ public class AALocationPickerController: AAViewController, CLLocationManagerDele // // } - public func mapView(mapView: MKMapView, regionWillChangeAnimated animated: Bool) { + open func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { pinPoint.risePin(true, animated: true) } - public func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + open func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { pinPoint.risePin(false, animated: true) } @@ -62,40 +62,40 @@ public class AALocationPickerController: AAViewController, CLLocationManagerDele delegate?.locationPickerDidPicked(self, latitude: map.centerCoordinate.latitude, longitude: map.centerCoordinate.longitude) } - func updateAuthStatus(status: CLAuthorizationStatus) { - if (status == CLAuthorizationStatus.Denied) { + func updateAuthStatus(_ status: CLAuthorizationStatus) { + if (status == CLAuthorizationStatus.denied) { // User explictly denied access to maps showPlaceholderWithImage(UIImage.bundled("location_placeholder"), title: AALocalized("Placeholder_Location_Title"), subtitle: AALocalized("Placeholder_Location_Message")) - map.hidden = true - pinPoint.hidden = true + map.isHidden = true + pinPoint.isHidden = true navigationItem.rightBarButtonItem = nil - } else if (status == CLAuthorizationStatus.Restricted || status == CLAuthorizationStatus.NotDetermined) { + } else if (status == CLAuthorizationStatus.restricted || status == CLAuthorizationStatus.notDetermined) { // App doesn't complete auth request - map.hidden = false - pinPoint.hidden = false - navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationDone"), style: .Done, target: self, action: #selector(AALocationPickerController.doneDidTap)) + map.isHidden = false + pinPoint.isHidden = false + navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationDone"), style: .done, target: self, action: #selector(AALocationPickerController.doneDidTap)) hidePlaceholder() } else { // Authorised - map.hidden = false - pinPoint.hidden = false - navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationDone"), style: .Done, target: self, action: #selector(AALocationPickerController.doneDidTap)) + map.isHidden = false + pinPoint.isHidden = false + navigationItem.rightBarButtonItem = UIBarButtonItem(title: AALocalized("NavigationDone"), style: .done, target: self, action: #selector(AALocationPickerController.doneDidTap)) hidePlaceholder() } } - override public func viewDidLayoutSubviews() { + override open func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() map.frame = self.view.bounds pinPoint.centerIn(self.view.bounds) } - public func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) { + open func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { updateAuthStatus(status) } - public func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + open func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { let location = locations.first! map.setRegion(MKCoordinateRegion(center: location.coordinate, span: MKCoordinateSpanMake(0.05, 0.05)), animated: true) locationManager.stopUpdatingLocation() @@ -103,6 +103,6 @@ public class AALocationPickerController: AAViewController, CLLocationManagerDele } public protocol AALocationPickerControllerDelegate { - func locationPickerDidPicked(controller: AALocationPickerController, latitude: Double, longitude: Double) - func locationPickerDidCancelled(controller: AALocationPickerController) -} \ No newline at end of file + func locationPickerDidPicked(_ controller: AALocationPickerController, latitude: Double, longitude: Double) + func locationPickerDidCancelled(_ controller: AALocationPickerController) +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AACollectionViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AACollectionViewController.swift index 6ff394ffa3..40a01c6d22 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AACollectionViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AACollectionViewController.swift @@ -4,41 +4,41 @@ import Foundation -public class AACollectionViewController: AAViewController, UICollectionViewDelegate, UICollectionViewDataSource { +open class AACollectionViewController: AAViewController, UICollectionViewDelegate, UICollectionViewDataSource { - public var collectionView:UICollectionView! + open var collectionView:UICollectionView! public init(collectionLayout: UICollectionViewLayout) { super.init() - collectionView = UICollectionView(frame: CGRectZero, collectionViewLayout: collectionLayout) + collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: collectionLayout) } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public override func loadView() { + open override func loadView() { super.loadView() collectionView.delegate = self collectionView.dataSource = self view.addSubview(collectionView) - navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: UIBarButtonItemStyle.Plain, target: nil, action: nil) + navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: UIBarButtonItemStyle.plain, target: nil, action: nil) } - public override func viewWillLayoutSubviews() { + open override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() collectionView.frame = view.bounds; } - public func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { fatalError("Not implemented!") } - public func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { + open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { fatalError("Not implemented!") } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAContentTableController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAContentTableController.swift index 4f245fe2e6..38d330da78 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAContentTableController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAContentTableController.swift @@ -6,16 +6,16 @@ import Foundation public enum AAContentTableStyle { - case SettingsPlain - case SettingsGrouped - case Plain + case settingsPlain + case settingsGrouped + case plain } -public class AAContentTableController: AAManagedTableController, AAManagedTableControllerDelegate { +open class AAContentTableController: AAManagedTableController, AAManagedTableControllerDelegate { - private var isInLoad: Bool = false + fileprivate var isInLoad: Bool = false - public var autoSections = true + open var autoSections = true // Controller constructor @@ -24,7 +24,7 @@ public class AAContentTableController: AAManagedTableController, AAManagedTableC self.managedTableDelegate = self - self.autoSections = style != .Plain + self.autoSections = style != .plain } public required init(coder aDecoder: NSCoder) { @@ -33,7 +33,7 @@ public class AAContentTableController: AAManagedTableController, AAManagedTableC // DSL Implementation - public func section(closure: (s: AAManagedSection) -> ()) { + open func section(_ closure: (_ s: AAManagedSection) -> ()) -> AAManagedSection { if !isInLoad { fatalError("Unable to change sections not during tableDidLoad method call") } @@ -47,14 +47,15 @@ public class AAContentTableController: AAManagedTableController, AAManagedTableC s.headerHeight = 0 } } - closure(s: s) + closure(s) + return s } - public func search(cell: C.Type, @noescape closure: (s: AAManagedSearchConfig) -> ()) { + open func search(_ cell: C.Type, closure: (_ s: AAManagedSearchConfig) -> ()) where C: AABindedSearchCell, C: UITableViewCell { managedTable.search(cell, closure: closure) } - public func afterTableCreated() { + open func afterTableCreated() { if autoSections { managedTable.sections.last?.footerHeight = 30 } @@ -62,25 +63,25 @@ public class AAContentTableController: AAManagedTableController, AAManagedTableC // Implement it in subclass - public func tableWillLoad() { + open func tableWillLoad() { } - public func tableDidLoad() { + open func tableDidLoad() { } - public func tableWillBind(binder: AABinder) { + open func tableWillBind(_ binder: AABinder) { } - public func tableWillUnbind(binder: AABinder) { + open func tableWillUnbind(_ binder: AABinder) { } // Delegate implementation - public func managedTableLoad(controller: AAManagedTableController, table: AAManagedTable) { + open func managedTableLoad(_ controller: AAManagedTableController, table: AAManagedTable) { isInLoad = true table.beginUpdates() tableDidLoad() @@ -89,15 +90,15 @@ public class AAContentTableController: AAManagedTableController, AAManagedTableC isInLoad = false } - public func managedTableBind(controller: AAManagedTableController, table: AAManagedTable, binder: AABinder) { + open func managedTableBind(_ controller: AAManagedTableController, table: AAManagedTable, binder: AABinder) { tableWillBind(binder) } - public func managedTableUnbind(controller: AAManagedTableController, table: AAManagedTable, binder: AABinder) { + open func managedTableUnbind(_ controller: AAManagedTableController, table: AAManagedTable, binder: AABinder) { tableWillUnbind(binder) } - public func managedTableWillLoad(controller: AAManagedTableController) { + open func managedTableWillLoad(_ controller: AAManagedTableController) { tableWillLoad() } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAImagePickerController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAImagePickerController.swift index 3c7cdc2479..fce7d62e6b 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAImagePickerController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAImagePickerController.swift @@ -4,8 +4,8 @@ import Foundation -public class AAImagePickerController: UIImagePickerController { - public override func preferredStatusBarStyle() -> UIStatusBarStyle { +open class AAImagePickerController: UIImagePickerController { + open override var preferredStatusBarStyle : UIStatusBarStyle { return ActorSDK.sharedActor().style.vcStatusBarStyle } @@ -26,4 +26,4 @@ public class AAImagePickerController: UIImagePickerController { // super.viewWillDisappear(animated) // UIApplication.sharedApplication().setStatusBarHidden(false, withAnimation: UIStatusBarAnimation.Fade) // } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAManagedRange.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAManagedRange.swift index 0f5c9c6681..813293a902 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAManagedRange.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAManagedRange.swift @@ -10,95 +10,95 @@ public protocol AAManagedRange { // Initing Table - func initTable(table: AAManagedTable) + func initTable(_ table: AAManagedTable) // Total items count - func rangeNumberOfItems(table: AAManagedTable) -> Int + func rangeNumberOfItems(_ table: AAManagedTable) -> Int // Cell - func rangeCellHeightForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat + func rangeCellHeightForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat - func rangeCellForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell + func rangeCellForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell // Selection - func rangeCanSelect(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool + func rangeCanSelect(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool - func rangeSelect(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool + func rangeSelect(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool // Copying - func rangeCanCopy(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool + func rangeCanCopy(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool - func rangeCopy(table: AAManagedTable, indexPath: AARangeIndexPath) + func rangeCopy(_ table: AAManagedTable, indexPath: AARangeIndexPath) // Delete - func rangeCanDelete(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool + func rangeCanDelete(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool - func rangeDelete(table: AAManagedTable, indexPath: AARangeIndexPath) + func rangeDelete(_ table: AAManagedTable, indexPath: AARangeIndexPath) // Binding - func rangeBind(table: AAManagedTable, binder: AABinder) + func rangeBind(_ table: AAManagedTable, binder: AABinder) - func rangeUnbind(table: AAManagedTable, binder: AABinder) + func rangeUnbind(_ table: AAManagedTable, binder: AABinder) } // Default implementations of ACManagedRangeDelegate public extension AAManagedRange { - public func rangeCanSelect(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { + public func rangeCanSelect(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { // Do nothing return false } - public func rangeSelect(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { + public func rangeSelect(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { // Do nothing return false } - public func rangeCanCopy(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { + public func rangeCanCopy(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { // Do nothing return false } - public func rangeCopy(table: AAManagedTable, indexPath: AARangeIndexPath) { + public func rangeCopy(_ table: AAManagedTable, indexPath: AARangeIndexPath) { // Do nothing } - public func rangeCanDelete(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { + public func rangeCanDelete(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { // Do nothing return false } - public func rangeDelete(table: AAManagedTable, indexPath: AARangeIndexPath) { + public func rangeDelete(_ table: AAManagedTable, indexPath: AARangeIndexPath) { // Do nothing } - public func rangeBind(table: AAManagedTable, binder: AABinder) { + public func rangeBind(_ table: AAManagedTable, binder: AABinder) { // Do nothing } - public func rangeUnbind(table: AAManagedTable, binder: AABinder) { + public func rangeUnbind(_ table: AAManagedTable, binder: AABinder) { // Do nothing } } -public class AARangeIndexPath { +open class AARangeIndexPath { - public let section: Int - public let range: Int - public let item: Int - public let indexPath: NSIndexPath + open let section: Int + open let range: Int + open let item: Int + open let indexPath: IndexPath - public init(section: Int, range: Int, item: Int, indexPath: NSIndexPath) { + public init(section: Int, range: Int, item: Int, indexPath: IndexPath) { self.section = section self.range = range self.item = item self.indexPath = indexPath } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAManagedSection.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAManagedSection.swift index d8eafc7c4e..b45fd5bc20 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAManagedSection.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAManagedSection.swift @@ -4,24 +4,24 @@ import Foundation -public class AAManagedSection { +open class AAManagedSection { - public var headerHeight: Double = 0 - public var footerHeight: Double = 0 + open var headerHeight: Double = 0 + open var footerHeight: Double = 0 - public var headerText: String? = nil - public var footerText: String? = nil + open var headerText: String? = nil + open var footerText: String? = nil - public let index: Int + open let index: Int - public var autoSeparatorTopOffset: Int = 0 - public var autoSeparatorBottomOffset: Int = 0 - public var autoSeparators: Bool = false - public var autoSeparatorsInset: CGFloat = 15.0 + open var autoSeparatorTopOffset: Int = 0 + open var autoSeparatorBottomOffset: Int = 0 + open var autoSeparators: Bool = false + open var autoSeparatorsInset: CGFloat = 15.0 - public var regions: [AAManagedRange] = [AAManagedRange]() + open var regions: [AAManagedRange] = [AAManagedRange]() - public unowned let table: AAManagedTable + open unowned let table: AAManagedTable public init(table: AAManagedTable, index: Int) { self.index = index @@ -30,7 +30,7 @@ public class AAManagedSection { // Items count - public func numberOfItems(managedTable: AAManagedTable) -> Int { + open func numberOfItems(_ managedTable: AAManagedTable) -> Int { var res = 0 for r in regions { res += r.rangeNumberOfItems(managedTable) @@ -40,12 +40,12 @@ public class AAManagedSection { // Cells - public func cellHeightForItem(managedTable: AAManagedTable, indexPath: NSIndexPath) -> CGFloat { + open func cellHeightForItem(_ managedTable: AAManagedTable, indexPath: IndexPath) -> CGFloat { let r = findCell(managedTable, indexPath: indexPath) return r.cells.rangeCellHeightForItem(managedTable, indexPath: r.index) } - public func cellForItem(managedTable: AAManagedTable, indexPath: NSIndexPath) -> UITableViewCell { + open func cellForItem(_ managedTable: AAManagedTable, indexPath: IndexPath) -> UITableViewCell { let r = findCell(managedTable, indexPath: indexPath) let res = r.cells.rangeCellForItem(managedTable, indexPath: r.index) @@ -54,7 +54,7 @@ public class AAManagedSection { // Top separator - if managedTable.style == .Plain { + if managedTable.style == .plain { // Don't show for plain style cell.topSeparatorVisible = false @@ -62,21 +62,21 @@ public class AAManagedSection { // Showing only for top cell cell.topSeparatorLeftInset = 0 - cell.topSeparatorVisible = indexPath.row == autoSeparatorTopOffset + cell.topSeparatorVisible = (indexPath as NSIndexPath).row == autoSeparatorTopOffset } // Bottom separator - if indexPath.row >= autoSeparatorTopOffset { + if (indexPath as NSIndexPath).row >= autoSeparatorTopOffset { cell.bottomSeparatorVisible = true - if managedTable.style == .Plain { + if managedTable.style == .plain { // Don't make last full line separator cell.bottomSeparatorLeftInset = autoSeparatorsInset } else { - if indexPath.row == numberOfItems(managedTable) - 1 { + if (indexPath as NSIndexPath).row == numberOfItems(managedTable) - 1 { cell.bottomSeparatorLeftInset = 0 } else { cell.bottomSeparatorLeftInset = autoSeparatorsInset @@ -95,36 +95,36 @@ public class AAManagedSection { // Selection - func canSelect(managedTable: AAManagedTable, indexPath: NSIndexPath) -> Bool { + func canSelect(_ managedTable: AAManagedTable, indexPath: IndexPath) -> Bool { let r = findCell(managedTable, indexPath: indexPath) return r.cells.rangeCanSelect(managedTable, indexPath: r.index) } - func select(managedTable: AAManagedTable, indexPath: NSIndexPath) -> Bool { + func select(_ managedTable: AAManagedTable, indexPath: IndexPath) -> Bool { let r = findCell(managedTable, indexPath: indexPath) return r.cells.rangeSelect(managedTable, indexPath: r.index) } // Copying - func canCopy(managedTable: AAManagedTable, indexPath: NSIndexPath) -> Bool { + func canCopy(_ managedTable: AAManagedTable, indexPath: IndexPath) -> Bool { let r = findCell(managedTable, indexPath: indexPath) return r.cells.rangeCanCopy(managedTable, indexPath: r.index) } - func copy(managedTable: AAManagedTable, indexPath: NSIndexPath) { + func copy(_ managedTable: AAManagedTable, indexPath: IndexPath) { let r = findCell(managedTable, indexPath: indexPath) r.cells.rangeCopy(managedTable, indexPath: r.index) } // Deletion - func canDelete(managedTable: AAManagedTable, indexPath: NSIndexPath) -> Bool { + func canDelete(_ managedTable: AAManagedTable, indexPath: IndexPath) -> Bool { let r = findCell(managedTable, indexPath: indexPath) return r.cells.rangeCanDelete(managedTable, indexPath: r.index) } - func delete(managedTable: AAManagedTable, indexPath: NSIndexPath) { + func delete(_ managedTable: AAManagedTable, indexPath: IndexPath) { let r = findCell(managedTable, indexPath: indexPath) r.cells.rangeDelete(managedTable, indexPath: r.index) } @@ -132,13 +132,13 @@ public class AAManagedSection { // Binding - func bind(managedTable: AAManagedTable, binder: AABinder) { + func bind(_ managedTable: AAManagedTable, binder: AABinder) { for s in regions { s.rangeBind(managedTable, binder: binder) } } - func unbind(managedTable: AAManagedTable, binder: AABinder) { + func unbind(_ managedTable: AAManagedTable, binder: AABinder) { for s in regions { s.rangeUnbind(managedTable, binder: binder) } @@ -146,20 +146,20 @@ public class AAManagedSection { // Private Tools - private func findCell(managedTable: AAManagedTable, indexPath: NSIndexPath) -> CellSearchResult { + fileprivate func findCell(_ managedTable: AAManagedTable, indexPath: IndexPath) -> CellSearchResult { var prevLength = 0 for i in 0.. AAManagedSection { + public func setSeparatorsTopOffset(_ offset: Int) -> AAManagedSection { self.autoSeparatorTopOffset = offset return self } - public func setFooterText(footerText: String) -> AAManagedSection { + public func setFooterText(_ footerText: String) -> AAManagedSection { self.footerText = footerText return self } - public func setHeaderText(headerText: String) -> AAManagedSection { + public func setHeaderText(_ headerText: String) -> AAManagedSection { self.headerText = headerText return self } - public func setFooterHeight(footerHeight: Double) -> AAManagedSection { + public func setFooterHeight(_ footerHeight: Double) -> AAManagedSection { self.footerHeight = footerHeight return self } - public func setHeaderHeight(headerHeight: Double) -> AAManagedSection { + public func setHeaderHeight(_ headerHeight: Double) -> AAManagedSection { self.headerHeight = headerHeight return self } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAManagedTable.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAManagedTable.swift index eaed3dc426..4d31247c92 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAManagedTable.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAManagedTable.swift @@ -4,66 +4,66 @@ import Foundation -public class AAManagedTable { +open class AAManagedTable { //------------------------------------------------------------------------// // Controller of table - public let controller: UIViewController + open let controller: UIViewController // Table view - public let style: AAContentTableStyle - public let tableView: UITableView - public var tableViewDelegate: UITableViewDelegate { get { return baseDelegate } } - public var tableViewDataSource: UITableViewDataSource { get { return baseDelegate } } + open let style: AAContentTableStyle + open let tableView: UITableView + open var tableViewDelegate: UITableViewDelegate { get { return baseDelegate } } + open var tableViewDataSource: UITableViewDataSource { get { return baseDelegate } } // Scrolling closure - public var tableScrollClosure: ((tableView: UITableView) -> ())? + open var tableScrollClosure: ((_ tableView: UITableView) -> ())? // Is fade in/out animated - public var fadeShowing = false + open var fadeShowing = false // Sections of table - public var sections: [AAManagedSection] = [AAManagedSection]() + open var sections: [AAManagedSection] = [AAManagedSection]() // Fixed Height - public var fixedHeight: CGFloat? + open var fixedHeight: CGFloat? // Can Edit All rows - public var canEditAll: Bool? + open var canEditAll: Bool? // Can Delete All rows - public var canDeleteAll: Bool? + open var canDeleteAll: Bool? // Is Table in editing mode - public var isEditing: Bool { + open var isEditing: Bool { get { - return tableView.editing + return tableView.isEditing } } // Is updating sections - private var isUpdating = false + fileprivate var isUpdating = false // Reference to table view delegate/data source - private var baseDelegate: AMBaseTableDelegate! + fileprivate var baseDelegate: AMBaseTableDelegate! // Search - private var isSearchInited: Bool = false - private var isSearchAutoHide: Bool = false - private var searchDisplayController: UISearchDisplayController! - private var searchManagedController: AnyObject! + fileprivate var isSearchInited: Bool = false + fileprivate var isSearchAutoHide: Bool = false + fileprivate var searchDisplayController: UISearchDisplayController! + fileprivate var searchManagedController: AnyObject! //------------------------------------------------------------------------// @@ -72,7 +72,7 @@ public class AAManagedTable { self.controller = controller self.tableView = tableView - if style == .SettingsGrouped { + if style == .settingsGrouped { self.baseDelegate = AMGrouppedTableDelegate(data: self) } else { self.baseDelegate = AMPlainTableDelegate(data: self) @@ -86,14 +86,14 @@ public class AAManagedTable { // Entry point to adding - public func beginUpdates() { + open func beginUpdates() { if isUpdating { fatalError("Already updating table") } isUpdating = true } - public func addSection(autoSeparator: Bool = false) -> AAManagedSection { + open func addSection(_ autoSeparator: Bool = false) -> AAManagedSection { if !isUpdating { fatalError("Table is not in updating mode") } @@ -104,7 +104,7 @@ public class AAManagedTable { return res } - public func search(cell: C.Type, @noescape closure: (s: AAManagedSearchConfig) -> ()) { + open func search(_ cell: C.Type, closure: (_ s: AAManagedSearchConfig) -> ()) where C: AABindedSearchCell, C: UITableViewCell { if !isUpdating { fatalError("Table is not in updating mode") @@ -120,7 +120,7 @@ public class AAManagedTable { let config = AAManagedSearchConfig() - closure(s: config) + closure(config) // Creating search source @@ -130,7 +130,7 @@ public class AAManagedTable { self.isSearchAutoHide = config.isSearchAutoHide } - public func endUpdates() { + open func endUpdates() { if !isUpdating { fatalError("Table is not in editable mode") } @@ -139,23 +139,23 @@ public class AAManagedTable { // Reloading table - public func reload() { + open func reload() { self.tableView.reloadData() } - public func reload(section: Int) { - self.tableView.reloadSections(NSIndexSet(index: section), withRowAnimation: .Automatic) + open func reload(_ section: Int) { + self.tableView.reloadSections(IndexSet(integer: section), with: .automatic) } // Binding methods - public func bind(binder: AABinder) { + open func bind(_ binder: AABinder) { for s in sections { s.bind(self, binder: binder) } } - public func unbind(binder: AABinder) { + open func unbind(_ binder: AABinder) { for s in sections { s.unbind(self, binder: binder) } @@ -163,21 +163,21 @@ public class AAManagedTable { // Show/hide table - public func showTable() { + open func showTable() { if isUpdating || !fadeShowing { self.tableView.alpha = 1 } else { - UIView.animateWithDuration(0.3, animations: { () -> Void in + UIView.animate(withDuration: 0.3, animations: { () -> Void in self.tableView.alpha = 1 }) } } - public func hideTable() { + open func hideTable() { if isUpdating || !fadeShowing { self.tableView.alpha = 0 } else { - UIView.animateWithDuration(0.3, animations: { () -> Void in + UIView.animate(withDuration: 0.3, animations: { () -> Void in self.tableView.alpha = 0 }) } @@ -185,7 +185,7 @@ public class AAManagedTable { // Controller callbacks - public func controllerViewWillDisappear(animated: Bool) { + open func controllerViewWillDisappear(_ animated: Bool) { // Auto close search on leaving controller if isSearchAutoHide { @@ -195,14 +195,14 @@ public class AAManagedTable { } } - public func controllerViewDidDisappear(animated: Bool) { + open func controllerViewDidDisappear(_ animated: Bool) { // Auto close search on leaving controller // searchDisplayController?.setActive(false, animated: animated) } - public func controllerViewWillAppear(animated: Bool) { + open func controllerViewWillAppear(_ animated: Bool) { // Search bar dissapear fixing @@ -224,14 +224,14 @@ public class AAManagedTable { // Status bar styles - if (searchDisplayController != nil && searchDisplayController!.active) { + if (searchDisplayController != nil && searchDisplayController!.isActive) { // If search is active: apply search status bar style - UIApplication.sharedApplication().setStatusBarStyle(ActorSDK.sharedActor().style.searchStatusBarStyle, animated: true) + UIApplication.shared.setStatusBarStyle(ActorSDK.sharedActor().style.searchStatusBarStyle, animated: true) } else { // If search is not active: apply main status bar style - UIApplication.sharedApplication().setStatusBarStyle(ActorSDK.sharedActor().style.vcStatusBarStyle, animated: true) + UIApplication.shared.setStatusBarStyle(ActorSDK.sharedActor().style.vcStatusBarStyle, animated: true) } } } @@ -240,8 +240,8 @@ public class AAManagedTable { public extension AAManagedTable { - public func section(closure: (s: AAManagedSection) -> ()){ - closure(s: addSection(true)) + public func section(_ closure: (_ s: AAManagedSection) -> ()){ + closure(addSection(true)) } } @@ -249,15 +249,15 @@ public extension AAManagedTable { private class AMPlainTableDelegate: AMBaseTableDelegate { - @objc func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + @objc func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return CGFloat(data.sections[section].headerHeight) } - @objc func tableView(tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + @objc func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { return CGFloat(data.sections[section].footerHeight) } - @objc func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + @objc func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { if (data.sections[section].headerText == nil) { return UIView() } else { @@ -265,7 +265,7 @@ private class AMPlainTableDelegate: AMBaseTableDelegate { } } - @objc func tableView(tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + @objc func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { if (data.sections[section].footerText == nil) { return UIView() } else { @@ -277,12 +277,12 @@ private class AMPlainTableDelegate: AMBaseTableDelegate { private class AMGrouppedTableDelegate: AMBaseTableDelegate { - @objc func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + @objc func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { let header: UITableViewHeaderFooterView = view as! UITableViewHeaderFooterView header.textLabel!.textColor = ActorSDK.sharedActor().style.cellHeaderColor } - @objc func tableView(tableView: UITableView, willDisplayFooterView view: UIView, forSection section: Int) { + @objc func tableView(_ tableView: UITableView, willDisplayFooterView view: UIView, forSection section: Int) { let header: UITableViewHeaderFooterView = view as! UITableViewHeaderFooterView header.textLabel!.textColor = ActorSDK.sharedActor().style.cellFooterColor } @@ -290,25 +290,25 @@ private class AMGrouppedTableDelegate: AMBaseTableDelegate { private class AMBaseTableDelegate: NSObject, UITableViewDelegate, UITableViewDataSource, UIScrollViewDelegate { - unowned private let data: AAManagedTable + unowned fileprivate let data: AAManagedTable init(data: AAManagedTable) { self.data = data } - @objc func numberOfSectionsInTableView(tableView: UITableView) -> Int { + @objc func numberOfSections(in tableView: UITableView) -> Int { return data.sections.count } - @objc func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + @objc func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return data.sections[section].numberOfItems(data) } - @objc func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { - return data.sections[indexPath.section].cellForItem(data, indexPath: indexPath) + @objc func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return data.sections[(indexPath as NSIndexPath).section].cellForItem(data, indexPath: indexPath) } - @objc func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + @objc func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { let text = data.sections[section].headerText if text != nil { return AALocalized(text!) @@ -317,7 +317,7 @@ private class AMBaseTableDelegate: NSObject, UITableViewDelegate, UITableViewDat } } - @objc func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? { + @objc func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { let text = data.sections[section].footerText if text != nil { return AALocalized(text!) @@ -327,125 +327,128 @@ private class AMBaseTableDelegate: NSObject, UITableViewDelegate, UITableViewDat } - @objc func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + @objc func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { if data.fixedHeight != nil { return data.fixedHeight! } - return data.sections[indexPath.section].cellHeightForItem(data, indexPath: indexPath) + return data.sections[(indexPath as NSIndexPath).section].cellHeightForItem(data, indexPath: indexPath) } - @objc func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { + @objc func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { if data.canEditAll != nil { return data.canEditAll! } - return (data.sections[indexPath.section].numberOfItems(data) > 0 ? data.sections[indexPath.section].canDelete(data, indexPath: indexPath) : false) + return (data.sections[(indexPath as NSIndexPath).section].numberOfItems(data) > 0 ? data.sections[(indexPath as NSIndexPath).section].canDelete(data, indexPath: indexPath) : false) } - @objc func tableView(tableView: UITableView, canMoveRowAtIndexPath indexPath: NSIndexPath) -> Bool { + @objc func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { return false } - @objc func tableView(tableView: UITableView, editingStyleForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCellEditingStyle { + @objc func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle { if data.canDeleteAll != nil { if data.canDeleteAll! { - return .Delete + return .delete } else { - return .None + return .none } } - return data.sections[indexPath.section].canDelete(data, indexPath: indexPath) ? .Delete : .None + return data.sections[(indexPath as NSIndexPath).section].canDelete(data, indexPath: indexPath) ? .delete : .none } - @objc func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { - return data.sections[indexPath.section].delete(data, indexPath: indexPath) + @objc func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { + return data.sections[(indexPath as NSIndexPath).section].delete(data, indexPath: indexPath) } - @objc func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { - let section = data.sections[indexPath.section] + @objc func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let section = data.sections[(indexPath as NSIndexPath).section] if section.canSelect(data, indexPath: indexPath) { if section.select(data, indexPath: indexPath) { - tableView.deselectRowAtIndexPath(indexPath, animated: true) + tableView.deselectRow(at: indexPath, animated: true) } } else { - tableView.deselectRowAtIndexPath(indexPath, animated: true) + tableView.deselectRow(at: indexPath, animated: true) } } - @objc func tableView(tableView: UITableView, canPerformAction action: Selector, forRowAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject?) -> Bool { + @objc func tableView(_ tableView: UITableView, canPerformAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) -> Bool { - if action == "copy:" { - let section = data.sections[indexPath.section] + if action == #selector(UIResponderStandardEditActions.copy(_:)) { + let section = data.sections[(indexPath as NSIndexPath).section] return section.canCopy(data, indexPath: indexPath) } return false } - @objc func tableView(tableView: UITableView, shouldShowMenuForRowAtIndexPath indexPath: NSIndexPath) -> Bool { - let section = data.sections[indexPath.section] + @objc func tableView(_ tableView: UITableView, shouldShowMenuForRowAt indexPath: IndexPath) -> Bool { + let section = data.sections[(indexPath as NSIndexPath).section] return section.canCopy(data, indexPath: indexPath) } - @objc func tableView(tableView: UITableView, performAction action: Selector, forRowAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject?) { - if action == "copy:" { - let section = data.sections[indexPath.section] + @objc func tableView(_ tableView: UITableView, performAction action: Selector, forRowAt indexPath: IndexPath, withSender sender: Any?) { + if action == #selector(UIResponderStandardEditActions.copy(_:)) { + let section = data.sections[(indexPath as NSIndexPath).section] if section.canCopy(data, indexPath: indexPath) { section.copy(data, indexPath: indexPath) } } } - @objc func scrollViewDidScroll(scrollView: UIScrollView) { + @objc func scrollViewDidScroll(_ scrollView: UIScrollView) { if (data.tableView == scrollView) { - data.tableScrollClosure?(tableView: data.tableView) + data.tableScrollClosure?(data.tableView) } } } -public class AAManagedSearchConfig { +open class AAManagedSearchConfig where BindCell: AABindedSearchCell, BindCell: UITableViewCell { - public var searchList: ARBindedDisplayList! - public var selectAction: ((BindCell.BindData) -> ())? - public var isSearchAutoHide: Bool = true - public var didBind: ((c: BindCell, d: BindCell.BindData) -> ())? + open var searchList: ARBindedDisplayList? + open var searchModel: ARSearchValueModel? + open var selectAction: ((BindCell.BindData) -> ())? + open var isSearchAutoHide: Bool = true + open var didBind: ((_ c: BindCell, _ d: BindCell.BindData) -> ())? } -private class AAManagedSearchController: NSObject, UISearchBarDelegate, UISearchDisplayDelegate, UITableViewDataSource, UITableViewDelegate, ARDisplayList_Listener { +private class AAManagedSearchController: NSObject, UISearchBarDelegate, UISearchDisplayDelegate, UITableViewDataSource, UITableViewDelegate, ARDisplayList_Listener, ARValueChangedListener where BindCell: AABindedSearchCell, BindCell: UITableViewCell { let config: AAManagedSearchConfig - let displayList: ARBindedDisplayList + let searchList: ARBindedDisplayList? + let searchModel: ARSearchValueModel? let searchDisplay: UISearchDisplayController init(config: AAManagedSearchConfig, controller: UIViewController, tableView: UITableView) { self.config = config - self.displayList = config.searchList + self.searchList = config.searchList + self.searchModel = config.searchModel let style = ActorSDK.sharedActor().style let searchBar = UISearchBar() // Styling Search bar - searchBar.searchBarStyle = UISearchBarStyle.Default - searchBar.translucent = false + searchBar.searchBarStyle = UISearchBarStyle.default + searchBar.isTranslucent = false searchBar.placeholder = "" // SearchBar placeholder animation fix // SearchBar background color searchBar.barTintColor = style.searchBackgroundColor.forTransparentBar() - searchBar.setBackgroundImage(Imaging.imageWithColor(style.searchBackgroundColor, size: CGSize(width: 1, height: 1)), forBarPosition: .Any, barMetrics: .Default) + searchBar.setBackgroundImage(Imaging.imageWithColor(style.searchBackgroundColor, size: CGSize(width: 1, height: 1)), for: .any, barMetrics: .default) searchBar.backgroundColor = style.searchBackgroundColor // SearchBar cancel color searchBar.tintColor = style.searchCancelColor // Apply keyboard color - searchBar.keyboardAppearance = style.isDarkApp ? UIKeyboardAppearance.Dark : UIKeyboardAppearance.Light + searchBar.keyboardAppearance = style.isDarkApp ? UIKeyboardAppearance.dark : UIKeyboardAppearance.light // SearchBar field color let fieldBg = Imaging.imageWithColor(style.searchFieldBgColor, size: CGSize(width: 14,height: 28)) .roundCorners(14, h: 28, roundSize: 4) - searchBar.setSearchFieldBackgroundImage(fieldBg.stretchableImageWithLeftCapWidth(7, topCapHeight: 0), forState: UIControlState.Normal) + searchBar.setSearchFieldBackgroundImage(fieldBg.stretchableImage(withLeftCapWidth: 7, topCapHeight: 0), for: UIControlState()) // SearchBar field text color for subView in searchBar.subviews { @@ -470,77 +473,106 @@ private class AAManagedSearchController BindCell.BindData { - return displayList.itemWithIndex(jint(indexPath.row)) as! BindCell.BindData + func objectAtIndexPath(_ indexPath: IndexPath) -> BindCell.BindData { + if let ds = searchList { + return ds.item(with: jint((indexPath as NSIndexPath).row)) as! BindCell.BindData + } else if let sm = searchModel { + let list = sm.getResults().get() as! JavaUtilList + return list.getWith(jint((indexPath as NSIndexPath).row)) as! BindCell.BindData + } else { + fatalError("No search model or search list is set!") + } } @objc func onCollectionChanged() { searchDisplay.searchResultsTableView.reloadData() } + @objc func onChanged(_ val: Any!, withModel valueModel: ARValue!) { + searchDisplay.searchResultsTableView.reloadData() + } + // Table view data - @objc func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return Int(displayList.size()); + @objc func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if let ds = searchList { + return Int(ds.size()) + } else if let sm = searchModel { + let list = sm.getResults().get() as! JavaUtilList + return Int(list.size()) + } else { + fatalError("No search model or search list is set!") + } } - @objc func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + @objc func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let item = objectAtIndexPath(indexPath) return BindCell.self.bindedCellHeight(item) } - @objc func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + @objc func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let item = objectAtIndexPath(indexPath) let cell = tableView.dequeueCell(BindCell.self, indexPath: indexPath) as! BindCell cell.bind(item, search: nil) return cell } - @objc func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + @objc func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let item = objectAtIndexPath(indexPath) config.selectAction!(item) - // MainAppTheme.navigation.applyStatusBar() } // Search updating - @objc func searchBar(searchBar: UISearchBar, textDidChange searchText: String) { - let normalized = searchText.trim().lowercaseString - if (normalized.length > 0) { - displayList.initSearchWithQuery(normalized, withRefresh: false) + @objc func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + if let ds = searchList { + let normalized = searchText.trim().lowercased() + if (normalized.length > 0) { + ds.initSearch(withQuery: normalized, withRefresh: false) + } else { + ds.initEmpty() + } + } else if let sm = searchModel { + sm.queryChanged(with: searchText) } else { - displayList.initEmpty() + fatalError("No search model or search list is set!") } } // Search styling - @objc func searchDisplayControllerWillBeginSearch(controller: UISearchDisplayController) { - UIApplication.sharedApplication().setStatusBarStyle(ActorSDK.sharedActor().style.searchStatusBarStyle, animated: true) + @objc func searchDisplayControllerWillBeginSearch(_ controller: UISearchDisplayController) { + UIApplication.shared.setStatusBarStyle(ActorSDK.sharedActor().style.searchStatusBarStyle, animated: true) } - @objc func searchDisplayControllerWillEndSearch(controller: UISearchDisplayController) { - UIApplication.sharedApplication().setStatusBarStyle(ActorSDK.sharedActor().style.vcStatusBarStyle, animated: true) + @objc func searchDisplayControllerWillEndSearch(_ controller: UISearchDisplayController) { + UIApplication.shared.setStatusBarStyle(ActorSDK.sharedActor().style.vcStatusBarStyle, animated: true) } - @objc func searchDisplayController(controller: UISearchDisplayController, didShowSearchResultsTableView tableView: UITableView) { + @objc func searchDisplayController(_ controller: UISearchDisplayController, didShowSearchResultsTableView tableView: UITableView) { for v in tableView.subviews { if (v is UIImageView) { (v as! UIImageView).alpha = 0; diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAManagedTableController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAManagedTableController.swift index 685b21bb2d..ac16e6c015 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAManagedTableController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAManagedTableController.swift @@ -4,21 +4,21 @@ import Foundation -public class AAManagedTableController: AAViewController { +open class AAManagedTableController: AAViewController { - public let style: AAContentTableStyle + open let style: AAContentTableStyle - public var managedTableDelegate: AAManagedTableControllerDelegate? + open var managedTableDelegate: AAManagedTableControllerDelegate? - public let binder = AABinder() + open let binder = AABinder() - public var tableView: UITableView! + open var tableView: UITableView! - public var managedTable: AAManagedTable! + open var managedTable: AAManagedTable! - public var unbindOnDissapear: Bool = false + open var unbindOnDissapear: Bool = false - private var isBinded: Bool = false + fileprivate var isBinded: Bool = false public init(style: AAContentTableStyle) { self.style = style @@ -29,31 +29,31 @@ public class AAManagedTableController: AAViewController { fatalError("init(coder:) has not been implemented") } - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() // Creating tables let tableViewStyle: UITableViewStyle switch(style) { - case .Plain: - tableViewStyle = .Plain + case .plain: + tableViewStyle = .plain break - case .SettingsPlain: - tableViewStyle = .Plain + case .settingsPlain: + tableViewStyle = .plain break - case .SettingsGrouped: - tableViewStyle = .Grouped + case .settingsGrouped: + tableViewStyle = .grouped break } tableView = UITableView(frame: view.bounds, style: tableViewStyle) // Disabling separators as we use manual separators handling - tableView.separatorStyle = .None + tableView.separatorStyle = .none // Setting tableView and view bg color depends on table style - tableView.backgroundColor = style == .Plain ? appStyle.vcBgColor : appStyle.vcBackyardColor + tableView.backgroundColor = style == .plain ? appStyle.vcBgColor : appStyle.vcBackyardColor view.backgroundColor = tableView.backgroundColor // Useful for making table view with fixed row height @@ -74,11 +74,11 @@ public class AAManagedTableController: AAViewController { tableView.reloadData() } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { if let t = tableView { if let row = t.indexPathForSelectedRow { - t.deselectRowAtIndexPath(row, animated: animated) + t.deselectRow(at: row, animated: animated) } } @@ -105,7 +105,7 @@ public class AAManagedTableController: AAViewController { } } - public override func viewWillDisappear(animated: Bool) { + open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) // Stopping data binding here @@ -131,7 +131,7 @@ public class AAManagedTableController: AAViewController { } } - public override func viewDidDisappear(animated: Bool) { + open override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) if let m = managedTable { @@ -141,7 +141,7 @@ public class AAManagedTableController: AAViewController { } } - public override func viewWillLayoutSubviews() { + open override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() tableView.frame = view.bounds @@ -149,23 +149,23 @@ public class AAManagedTableController: AAViewController { } public protocol AAManagedTableControllerDelegate { - func managedTableWillLoad(controller: AAManagedTableController) - func managedTableLoad(controller: AAManagedTableController, table: AAManagedTable) - func managedTableBind(controller: AAManagedTableController, table: AAManagedTable, binder: AABinder) - func managedTableUnbind(controller: AAManagedTableController, table: AAManagedTable, binder: AABinder) + func managedTableWillLoad(_ controller: AAManagedTableController) + func managedTableLoad(_ controller: AAManagedTableController, table: AAManagedTable) + func managedTableBind(_ controller: AAManagedTableController, table: AAManagedTable, binder: AABinder) + func managedTableUnbind(_ controller: AAManagedTableController, table: AAManagedTable, binder: AABinder) } public extension AAManagedTableControllerDelegate { - public func managedTableLoad(controller: AAManagedTableController, table: AAManagedTable) { + public func managedTableLoad(_ controller: AAManagedTableController, table: AAManagedTable) { // Do nothing } - public func managedTableBind(controller: AAManagedTableController, table: AAManagedTable, binder: AABinder) { + public func managedTableBind(_ controller: AAManagedTableController, table: AAManagedTable, binder: AABinder) { // Do nothing } - public func managedTableUnbind(controller: AAManagedTableController, table: AAManagedTable, binder: AABinder) { + public func managedTableUnbind(_ controller: AAManagedTableController, table: AAManagedTable, binder: AABinder) { // Do nothing } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AANavigationController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AANavigationController.swift index c66ec0d58c..50afafe6c3 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AANavigationController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AANavigationController.swift @@ -4,15 +4,15 @@ import UIKit -public class AANavigationController: UINavigationController { +open class AANavigationController: UINavigationController { - private let binder = AABinder() + fileprivate let binder = AABinder() public init() { super.init(nibName: nil, bundle: nil) } - public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) { + public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } @@ -24,7 +24,7 @@ public class AANavigationController: UINavigationController { fatalError("init(coder:) has not been implemented") } - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() styleNavBar() @@ -43,19 +43,19 @@ public class AANavigationController: UINavigationController { // } } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) styleNavBar() - UIApplication.sharedApplication().setStatusBarStyle(ActorSDK.sharedActor().style.vcStatusBarStyle, animated: true) + UIApplication.shared.setStatusBarStyle(ActorSDK.sharedActor().style.vcStatusBarStyle, animated: true) } - public override func preferredStatusBarStyle() -> UIStatusBarStyle { + open override var preferredStatusBarStyle : UIStatusBarStyle { return ActorSDK.sharedActor().style.vcStatusBarStyle } - private func styleNavBar() { + fileprivate func styleNavBar() { navigationBar.titleTextAttributes = [NSForegroundColorAttributeName: ActorSDK.sharedActor().style.navigationTitleColor] navigationBar.tintColor = ActorSDK.sharedActor().style.navigationTintColor diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AATableViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AATableViewController.swift index be9db17f80..01ddd22b13 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AATableViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AATableViewController.swift @@ -4,14 +4,14 @@ import UIKit -public class AATableViewController: AAViewController, UITableViewDataSource, UITableViewDelegate { +open class AATableViewController: AAViewController, UITableViewDataSource, UITableViewDelegate { - public let tableView: UITableView - public let tableViewStyle: UITableViewStyle + open let tableView: UITableView + open let tableViewStyle: UITableViewStyle public init(style: UITableViewStyle) { tableViewStyle = style - tableView = UITableView(frame: CGRectZero, style: tableViewStyle) + tableView = UITableView(frame: CGRect.zero, style: tableViewStyle) super.init() } @@ -19,50 +19,50 @@ public class AATableViewController: AAViewController, UITableViewDataSource, UIT fatalError("init(coder:) has not been implemented") } - public override func loadView() { + open override func loadView() { super.loadView() tableView.delegate = self tableView.dataSource = self view.addSubview(tableView) - navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: UIBarButtonItemStyle.Plain, target: nil, action: nil) + navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: UIBarButtonItemStyle.plain, target: nil, action: nil) } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { if let row = tableView.indexPathForSelectedRow { - tableView.deselectRowAtIndexPath(row, animated: animated) + tableView.deselectRow(at: row, animated: animated) } super.viewWillAppear(animated) } - public override func viewWillLayoutSubviews() { + open override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() tableView.frame = view.bounds; } - public override func setEditing(editing: Bool, animated: Bool) { + open override func setEditing(_ editing: Bool, animated: Bool) { super.setEditing(editing, animated: animated) tableView.setEditing(editing, animated: animated) } - private func placeholderViewFrame() -> CGRect { + fileprivate func placeholderViewFrame() -> CGRect { let navigationBarHeight: CGFloat = 64.5 return CGRect(x: 0, y: navigationBarHeight, width: view.bounds.size.width, height: view.bounds.size.height - navigationBarHeight) } - public func numberOfSectionsInTableView(tableView: UITableView) -> Int { + open func numberOfSections(in tableView: UITableView) -> Int { return 1 } - public func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 0 } - public func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return UITableViewCell() } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAViewController.swift index cd523e18e3..d82c183b8e 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/AAViewController.swift @@ -8,24 +8,24 @@ import RSKImageCropper import DZNWebViewController import SafariServices -public class AAViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate, RSKImageCropViewControllerDelegate, UIViewControllerTransitioningDelegate { +open class AAViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate, RSKImageCropViewControllerDelegate, UIViewControllerTransitioningDelegate { // MARK: - // MARK: Public vars var placeholder = AABigPlaceholderView(topOffset: 0) - var pendingPickClosure: ((image: UIImage) -> ())? + var pendingPickClosure: ((_ image: UIImage) -> ())? var popover: UIPopoverController? // Content type for view tracking - public var content: ACPage? + open var content: ACPage? // Data for views - public var autoTrack: Bool = false { + open var autoTrack: Bool = false { didSet { if self.autoTrack { if let u = self.uid { @@ -38,29 +38,29 @@ public class AAViewController: UIViewController, UINavigationControllerDelegate, } } - public var uid: Int! { + open var uid: Int! { didSet { if self.uid != nil { self.user = Actor.getUserWithUid(jint(self.uid)) - self.isBot = user.isBot().boolValue + self.isBot = user.isBot() } } } - public var user: ACUserVM! - public var isBot: Bool! + open var user: ACUserVM! + open var isBot: Bool! - public var gid: Int! { + open var gid: Int! { didSet { if self.gid != nil { self.group = Actor.getGroupWithGid(jint(self.gid)) } } } - public var group: ACGroupVM! + open var group: ACGroupVM! // Style - public var appStyle: ActorStyle { + open var appStyle: ActorStyle { get { return ActorSDK.sharedActor().style } @@ -69,42 +69,42 @@ public class AAViewController: UIViewController, UINavigationControllerDelegate, public init() { super.init(nibName: nil, bundle: nil) - navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: UIBarButtonItemStyle.Plain, target: nil, action: nil) + navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: UIBarButtonItemStyle.plain, target: nil, action: nil) } - public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) { + public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: UIBarButtonItemStyle.Plain, target: nil, action: nil) + navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: UIBarButtonItemStyle.plain, target: nil, action: nil) } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public override func preferredStatusBarStyle() -> UIStatusBarStyle { + open override var preferredStatusBarStyle : UIStatusBarStyle { return ActorSDK.sharedActor().style.vcStatusBarStyle } - public func showPlaceholderWithImage(image: UIImage?, title: String?, subtitle: String?) { + open func showPlaceholderWithImage(_ image: UIImage?, title: String?, subtitle: String?) { placeholder.setImage(image, title: title, subtitle: subtitle) showPlaceholder() } - public func showPlaceholder() { + open func showPlaceholder() { if placeholder.superview == nil { placeholder.frame = view.bounds view.addSubview(placeholder) } } - public func hidePlaceholder() { + open func hidePlaceholder() { if placeholder.superview != nil { placeholder.removeFromSuperview() } } - public func shakeView(view: UIView, originalX: CGFloat) { + open func shakeView(_ view: UIView, originalX: CGFloat) { var r = view.frame r.origin.x = originalX let originalFrame = r @@ -112,11 +112,11 @@ public class AAViewController: UIViewController, UINavigationControllerDelegate, rFirst.origin.x = r.origin.x + 4 r.origin.x = r.origin.x - 4 - UIView.animateWithDuration(0.05, delay: 0.0, options: .Autoreverse, animations: { () -> Void in + UIView.animate(withDuration: 0.05, delay: 0.0, options: .autoreverse, animations: { () -> Void in view.frame = rFirst }) { (finished) -> Void in if (finished) { - UIView.animateWithDuration(0.05, delay: 0.0, options: [.Repeat, .Autoreverse], animations: { () -> Void in + UIView.animate(withDuration: 0.05, delay: 0.0, options: [.repeat, .autoreverse], animations: { () -> Void in UIView.setAnimationRepeatCount(3) view.frame = r }, completion: { (finished) -> Void in @@ -128,116 +128,117 @@ public class AAViewController: UIViewController, UINavigationControllerDelegate, } } - public func pickAvatar(takePhoto:Bool, closure: (image: UIImage) -> ()) { + open func pickAvatar(_ takePhoto:Bool, closure: @escaping (_ image: UIImage) -> ()) { self.pendingPickClosure = closure let pickerController = AAImagePickerController() - pickerController.sourceType = (takePhoto ? UIImagePickerControllerSourceType.Camera : UIImagePickerControllerSourceType.PhotoLibrary) + pickerController.sourceType = (takePhoto ? UIImagePickerControllerSourceType.camera : UIImagePickerControllerSourceType.photoLibrary) pickerController.mediaTypes = [kUTTypeImage as String] pickerController.delegate = self - self.navigationController!.presentViewController(pickerController, animated: true, completion: nil) + self.navigationController!.present(pickerController, animated: true, completion: nil) } - public override func viewWillLayoutSubviews() { + open override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() - placeholder.frame = CGRectMake(0, 64, view.bounds.width, view.bounds.height - 64) + placeholder.frame = CGRect(x: 0, y: 64, width: view.bounds.width, height: view.bounds.height - 64) } - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if let c = content { ActorSDK.sharedActor().trackPageVisible(c) } if let u = uid { - Actor.onProfileOpenWithUid(jint(u)) + Actor.onProfileOpen(withUid: jint(u)) } } - public override func viewWillDisappear(animated: Bool) { + open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) if let c = content { ActorSDK.sharedActor().trackPageHidden(c) } if let u = uid { - Actor.onProfileClosedWithUid(jint(u)) + Actor.onProfileClosed(withUid: jint(u)) } } - public func dismiss() { - dismissViewControllerAnimated(true, completion: nil) + open func dismissController() { + self.dismiss(animated: true, completion: nil) } - public func presentInNavigation(controller: UIViewController) { + open func presentInNavigation(_ controller: UIViewController) { let navigation = AANavigationController() navigation.viewControllers = [controller] - presentViewController(navigation, animated: true, completion: nil) + present(navigation, animated: true, completion: nil) } - public func openUrl(url: String) { - if let url = NSURL(string: url) { + open func openUrl(_ url: String) { + if let url = URL(string: url) { if #available(iOS 9.0, *) { - self.presentElegantViewController(SFSafariViewController(URL: url)) + self.presentElegantViewController(SFSafariViewController(url: url)) } else { - self.presentElegantViewController(AANavigationController(rootViewController: DZNWebViewController(URL: url))) + self.presentElegantViewController(AANavigationController(rootViewController: DZNWebViewController(url: url))) } } } // Image pick and crop - public func cropImage(image: UIImage) { + open func cropImage(_ image: UIImage) { let cropController = RSKImageCropViewController(image: image) cropController.delegate = self - navigationController!.presentViewController(UINavigationController(rootViewController: cropController), animated: true, completion: nil) + navigationController!.present(UINavigationController(rootViewController: cropController), animated: true, completion: nil) } - public func imageCropViewController(controller: RSKImageCropViewController, didCropImage croppedImage: UIImage, usingCropRect cropRect: CGRect) { + open func imageCropViewController(_ controller: RSKImageCropViewController, didCropImage croppedImage: UIImage, usingCropRect cropRect: CGRect) { if (pendingPickClosure != nil){ - pendingPickClosure!(image: croppedImage) + pendingPickClosure!(croppedImage) } pendingPickClosure = nil - navigationController!.dismissViewControllerAnimated(true, completion: nil) + navigationController!.dismiss(animated: true, completion: nil) } - public func imageCropViewControllerDidCancelCrop(controller: RSKImageCropViewController) { + open func imageCropViewControllerDidCancelCrop(_ controller: RSKImageCropViewController) { pendingPickClosure = nil - navigationController!.dismissViewControllerAnimated(true, completion: nil) + navigationController!.dismiss(animated: true, completion: nil) } - public func imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) { - navigationController!.dismissViewControllerAnimated(true, completion: { () -> Void in + open func imagePickerController(_ picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) { + navigationController!.dismiss(animated: true, completion: { () -> Void in self.cropImage(image) }) } - public func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) { + open func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { let image = info[UIImagePickerControllerOriginalImage] as! UIImage - navigationController!.dismissViewControllerAnimated(true, completion: { () -> Void in + navigationController!.dismiss(animated: true, completion: { () -> Void in self.cropImage(image) }) } - public func imagePickerControllerDidCancel(picker: UIImagePickerController) { + open func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { pendingPickClosure = nil - self.dismissViewControllerAnimated(true, completion: nil) + self.dismiss(animated: true, completion: nil) } - public func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController, sourceViewController source: UIViewController) -> UIPresentationController? { - return ElegantPresentations.controller(presentedViewController: presented, presentingViewController: presenting, options: []) + open func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { + return ElegantPresentations.controller(presentedViewController: presented, presentingViewController: presenting!, options: []) } - public func presentElegantViewController(controller: UIViewController) { + open func presentElegantViewController(_ controller: UIViewController) { if AADevice.isiPad { - controller.modalPresentationStyle = .FormSheet - presentViewController(controller, animated: true, completion: nil) + controller.modalPresentationStyle = .formSheet + present(controller, animated: true, completion: nil) } else { - controller.modalPresentationStyle = .Custom - controller.transitioningDelegate = self - presentViewController(controller, animated: true, completion: nil) + // controller.modalPresentationStyle = .custom + // controller.modalPresentationStyle = .custom + // controller.transitioningDelegate = self + present(controller, animated: true, completion: nil) } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/Alerts.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/Alerts.swift index 0df2a078c6..c75fa6a442 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/Alerts.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/Alerts.swift @@ -9,25 +9,44 @@ private var actionShitReference = "_action_shit" public extension UIViewController { - public func alertUser(message: String) { - let controller = UIAlertController(title: nil, message: message, preferredStyle: UIAlertControllerStyle.Alert) - controller.addAction(UIAlertAction(title: AALocalized("AlertOk"), style: UIAlertActionStyle.Cancel, handler: nil)) - self.presentViewController(controller, animated: true, completion: nil) + public func alertUser(_ message: String) { + let controller = UIAlertController(title: nil, message: message, preferredStyle: UIAlertControllerStyle.alert) + controller.addAction(UIAlertAction(title: AALocalized("AlertOk"), style: UIAlertActionStyle.cancel, handler: nil)) + self.present(controller, animated: true, completion: nil) } - public func confirmAlertUser(message: String, action: String, tapYes: ()->(), tapNo: (()->())? = nil) { - let controller = UIAlertController(title: nil, message: message, preferredStyle: UIAlertControllerStyle.Alert) - controller.addAction(UIAlertAction(title: AALocalized(message), style: UIAlertActionStyle.Default, handler: { (alertView) -> () in + public func alertUser(_ message: String, tapYes: @escaping ()->()) { + let controller = UIAlertController(title: nil, message: message, preferredStyle: UIAlertControllerStyle.alert) + controller.addAction(UIAlertAction(title: AALocalized("AlertOk"), style: UIAlertActionStyle.cancel, handler: { (alertView) -> () in tapYes() })) - controller.addAction(UIAlertAction(title: AALocalized("AlertCancel"), style: UIAlertActionStyle.Cancel, handler: { (alertView) -> () in + self.present(controller, animated: true, completion: nil) + } + + public func confirmAlertUser(_ message: String, action: String, tapYes: @escaping ()->(), tapNo: (()->())? = nil) { + let controller = UIAlertController(title: nil, message: AALocalized(message), preferredStyle: UIAlertControllerStyle.alert) + controller.addAction(UIAlertAction(title: AALocalized(action), style: UIAlertActionStyle.default, handler: { (alertView) -> () in + tapYes() + })) + controller.addAction(UIAlertAction(title: AALocalized("AlertCancel"), style: UIAlertActionStyle.cancel, handler: { (alertView) -> () in tapNo?() })) - self.presentViewController(controller, animated: true, completion: nil) + self.present(controller, animated: true, completion: nil) } - public func confirmDangerSheetUser(action: String, tapYes: ()->(), tapNo: (()->())?) { - showActionSheet(nil, buttons: [], cancelButton: "AlertCancel", destructButton: action, sourceView: UIView(), sourceRect: CGRectZero) { (index) -> () in + public func confirmAlertUserDanger(_ message: String, action: String, tapYes: @escaping ()->(), tapNo: (()->())? = nil) { + let controller = UIAlertController(title: nil, message: AALocalized(message), preferredStyle: UIAlertControllerStyle.alert) + controller.addAction(UIAlertAction(title: AALocalized(action), style: UIAlertActionStyle.destructive, handler: { (alertView) -> () in + tapYes() + })) + controller.addAction(UIAlertAction(title: AALocalized("AlertCancel"), style: UIAlertActionStyle.cancel, handler: { (alertView) -> () in + tapNo?() + })) + self.present(controller, animated: true, completion: nil) + } + + public func confirmDangerSheetUser(_ action: String, tapYes: @escaping ()->(), tapNo: (()->())?) { + showActionSheet(nil, buttons: [], cancelButton: "AlertCancel", destructButton: action, sourceView: UIView(), sourceRect: CGRect.zero) { (index) -> () in if index == -2 { tapYes() } else { @@ -36,55 +55,55 @@ public extension UIViewController { } } - public func showActionSheet(title: String?, buttons: [String], cancelButton: String?, destructButton: String?, sourceView: UIView, sourceRect: CGRect, tapClosure: (index: Int) -> ()) { + public func showActionSheet(_ title: String?, buttons: [String], cancelButton: String?, destructButton: String?, sourceView: UIView, sourceRect: CGRect, tapClosure: @escaping (_ index: Int) -> ()) { - let controller = UIAlertController(title: title, message: nil, preferredStyle: UIAlertControllerStyle.ActionSheet) + let controller = UIAlertController(title: title, message: nil, preferredStyle: UIAlertControllerStyle.actionSheet) if cancelButton != nil { - controller.addAction(UIAlertAction(title: AALocalized(cancelButton), style: UIAlertActionStyle.Cancel, handler: { (alertView) -> () in - tapClosure(index: -1) + controller.addAction(UIAlertAction(title: AALocalized(cancelButton!), style: UIAlertActionStyle.cancel, handler: { (alertView) -> () in + tapClosure(-1) })) } if destructButton != nil { - controller.addAction(UIAlertAction(title: AALocalized(destructButton), style: UIAlertActionStyle.Destructive, handler: { (alertView) -> () in - tapClosure(index: -1) + controller.addAction(UIAlertAction(title: AALocalized(destructButton!), style: UIAlertActionStyle.destructive, handler: { (alertView) -> () in + tapClosure(-2) })) } for b in 0.. () in - tapClosure(index: b) + controller.addAction(UIAlertAction(title: AALocalized(buttons[b]), style: UIAlertActionStyle.default, handler: { (alertView) -> () in + tapClosure(b) })) } - self.presentViewController(controller, animated: true, completion: nil) + self.present(controller, animated: true, completion: nil) } - func showActionSheet(buttons: [String], cancelButton: String?, destructButton: String?, sourceView: UIView, sourceRect: CGRect, tapClosure: (index: Int) -> ()) { + func showActionSheet(_ buttons: [String], cancelButton: String?, destructButton: String?, sourceView: UIView, sourceRect: CGRect, tapClosure: @escaping (_ index: Int) -> ()) { showActionSheet(nil, buttons:buttons, cancelButton: cancelButton, destructButton: destructButton, sourceView: sourceView, sourceRect:sourceRect, tapClosure: tapClosure) } - func startEditText(@noescape closure: (AAEditTextControllerConfig) -> ()) { + func startEditText(_ closure: (AAEditTextControllerConfig) -> ()) { let config = AAEditTextControllerConfig() closure(config) config.check() let controller = AANavigationController(rootViewController: AAEditTextController(config: config)) if (AADevice.isiPad) { - controller.modalPresentationStyle = .FormSheet + controller.modalPresentationStyle = .formSheet } - self.presentViewController(controller, animated: true, completion: nil) + self.present(controller, animated: true, completion: nil) } - func startEditField(@noescape closure: (c: AAEditFieldControllerConfig) -> ()) { + func startEditField(_ closure: (_ c: AAEditFieldControllerConfig) -> ()) { let config = AAEditFieldControllerConfig() - closure(c: config) + closure(config) config.check() let controller = AANavigationController(rootViewController: AAEditFieldController(config: config)) if (AADevice.isiPad) { - controller.modalPresentationStyle = .FormSheet + controller.modalPresentationStyle = .formSheet } - self.presentViewController(controller, animated: true, completion: nil) + self.present(controller, animated: true, completion: nil) } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/Executions.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/Executions.swift index eceedc8066..0ff5c7dad5 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/Executions.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/Executions.swift @@ -6,16 +6,16 @@ import Foundation import MBProgressHUD public enum AAExecutionType { - case Normal - case Hidden - case Safe + case normal + case hidden + case safe } -public class AAMenuBuilder { +open class AAMenuBuilder { - public var tapClosure: ((index: Int) -> ())! - public var items = [String]() - public var closures: [(()->())?] = [] + open var tapClosure: ((_ index: Int) -> ())! + open var items = [String]() + open var closures: [(()->())?] = [] public init() { @@ -26,49 +26,49 @@ public class AAMenuBuilder { } } - public func add(title: String, closure: (()->())?) { + open func add(_ title: String, closure: (()->())?) { items.append(title) closures.append(closure) } } -public class AAExecutions { +open class AAExecutions { - public class func execute(promise: ARPromise) { + open class func execute(_ promise: ARPromise) { executePromise(promise) } - public class func execute(command: ACCommand) { + open class func execute(_ command: ACCommand) { execute(command, successBlock: nil, failureBlock: nil) } - public class func executePromise(promice: ARPromise){ + open class func executePromise(_ promice: ARPromise){ promice.startUserAction() } - public class func executePromise(promice: ARPromise, successBlock: ((val: Any?) -> Void)?, failureBlock: ((val: Any?) -> Void)? ){ + open class func executePromise(_ promice: ARPromise, successBlock: ((_ val: Any?) -> Void)?, failureBlock: ((_ val: Any?) -> Void)? ){ promice.startUserAction() promice.then { result in - successBlock!(val: result) + successBlock!(result) } } - public class func execute(command: ACCommand, type: AAExecutionType = .Normal, ignore: [String] = [], successBlock: ((val: Any?) -> Void)?, failureBlock: ((val: Any?) -> Void)?) { + open class func execute(_ command: ACCommand, type: AAExecutionType = .normal, ignore: [String] = [], successBlock: ((_ val: Any?) -> Void)?, failureBlock: ((_ val: Any?) -> Void)?) { var hud: MBProgressHUD? - if type != .Hidden { + if type != .hidden { hud = showProgress() } - command.startWithCallback(AACommandCallback(result: { (val:Any?) -> () in + command.start(with: AACommandCallback(result: { (val:Any?) -> () in dispatchOnUi { hud?.hide(true) - successBlock?(val: val) + successBlock?(val) } }, error: { (val) -> () in dispatchOnUi { hud?.hide(true) - if type == .Safe { + if type == .safe { // If unknown error, just try again var tryAgain = true @@ -76,7 +76,7 @@ public class AAExecutions { // If is in ignore list, just return to UI if ignore.contains(exception.tag) { - failureBlock?(val: val) + failureBlock?(val) return } @@ -86,32 +86,32 @@ public class AAExecutions { // Showing alert if tryAgain { - errorWithError(val, rep: { () -> () in + errorWithError(val!, rep: { () -> () in AAExecutions.execute(command, type: type, successBlock: successBlock, failureBlock: failureBlock) }, cancel: { () -> () in - failureBlock?(val: val) + failureBlock?(val) }) } else { - errorWithError(val, cancel: { () -> () in - failureBlock?(val: val) + errorWithError(val!, cancel: { () -> () in + failureBlock?(val) }) } } else { - failureBlock?(val: val) + failureBlock?(val) } } })) } - public class func errorWithError(e: AnyObject, rep:(()->())? = nil, cancel:(()->())? = nil) { + open class func errorWithError(_ e: AnyObject, rep:(()->())? = nil, cancel:(()->())? = nil) { error(Actor.getFormatter().formatErrorTextWithError(e), rep: rep, cancel: cancel) } - public class func errorWithTag(tag: String, rep:(()->())? = nil, cancel:(()->())? = nil) { - error(Actor.getFormatter().formatErrorTextWithTag(tag), rep: rep, cancel: cancel) + open class func errorWithTag(_ tag: String, rep:(()->())? = nil, cancel:(()->())? = nil) { + error(Actor.getFormatter().formatErrorText(withTag: tag), rep: rep, cancel: cancel) } - public class func error(message: String, rep:(()->())? = nil, cancel:(()->())? = nil) { + open class func error(_ message: String, rep:(()->())? = nil, cancel:(()->())? = nil) { if rep != nil { let d = UIAlertViewBlock(clickedClosure: { (index) -> () in if index > 0 { @@ -140,13 +140,13 @@ public class AAExecutions { } } - class private func showProgress() -> MBProgressHUD { - let window = UIApplication.sharedApplication().windows[1] + class fileprivate func showProgress() -> MBProgressHUD { + let window = UIApplication.shared.windows[1] let hud = MBProgressHUD(window: window) - hud.mode = MBProgressHUDMode.Indeterminate + hud.mode = MBProgressHUDMode.indeterminate hud.removeFromSuperViewOnHide = true window.addSubview(hud) - window.bringSubviewToFront(hud) + window.bringSubview(toFront: hud) hud.show(true) return hud } @@ -156,54 +156,55 @@ private var alertViewBlockReference = "_block_reference" @objc private class UIAlertViewBlock: NSObject, UIAlertViewDelegate { - private let clickedClosure: ((index: Int) -> ()) + fileprivate let clickedClosure: ((_ index: Int) -> ()) - init(clickedClosure: ((index: Int) -> ())) { + init(clickedClosure: @escaping ((_ index: Int) -> ())) { self.clickedClosure = clickedClosure } - @objc private func alertView(alertView: UIAlertView, clickedButtonAtIndex buttonIndex: Int) { - clickedClosure(index: buttonIndex) + @objc fileprivate func alertView(_ alertView: UIAlertView, clickedButtonAt buttonIndex: Int) { + clickedClosure(buttonIndex) } - @objc private func alertViewCancel(alertView: UIAlertView) { - clickedClosure(index: -1) + @objc fileprivate func alertViewCancel(_ alertView: UIAlertView) { + clickedClosure(-1) } } public extension UIViewController { - public func execute(command: ACCommand) { + public func execute(_ command: ACCommand) { AAExecutions.execute(command) } - public func executePromise(promise: ARPromise) { + public func executePromise(_ promise: ARPromise) -> ARPromise { AAExecutions.execute(promise) + return promise } - public func executePromise(promise: ARPromise, successBlock: ((val: Any?) -> Void)?, failureBlock: ((val: Any?) -> Void)?) { + public func executePromise(_ promise: ARPromise, successBlock: ((_ val: Any?) -> Void)?, failureBlock: ((_ val: Any?) -> Void)?) { AAExecutions.executePromise(promise, successBlock: successBlock, failureBlock: failureBlock) } - public func execute(command: ACCommand, successBlock: ((val: Any?) -> Void)?, failureBlock: ((val: Any?) -> Void)?) { + public func execute(_ command: ACCommand, successBlock: ((_ val: Any?) -> Void)?, failureBlock: ((_ val: Any?) -> Void)?) { AAExecutions.execute(command, successBlock: successBlock, failureBlock: failureBlock) } - public func execute(command: ACCommand, successBlock: ((val: Any?) -> Void)?) { + public func execute(_ command: ACCommand, successBlock: ((_ val: Any?) -> Void)?) { AAExecutions.execute(command, successBlock: successBlock, failureBlock: nil) } - public func executeSafe(command: ACCommand, ignore: [String] = [], successBlock: ((val: Any?) -> Void)? = nil) { - AAExecutions.execute(command, type: .Safe, ignore: ignore, successBlock: successBlock, failureBlock: { (val) -> () in - successBlock?(val: nil) + public func executeSafe(_ command: ACCommand, ignore: [String] = [], successBlock: ((_ val: Any?) -> Void)? = nil) { + AAExecutions.execute(command, type: .safe, ignore: ignore, successBlock: successBlock, failureBlock: { (val) -> () in + successBlock?(nil) }) } - public func executeSafeOnlySuccess(command: ACCommand, successBlock: ((val: Any?) -> Void)?) { - AAExecutions.execute(command, type: .Safe, ignore: [], successBlock: successBlock, failureBlock: nil) + public func executeSafeOnlySuccess(_ command: ACCommand, successBlock: ((_ val: Any?) -> Void)?) { + AAExecutions.execute(command, type: .safe, ignore: [], successBlock: successBlock, failureBlock: nil) } - public func executeHidden(command: ACCommand, successBlock: ((val: Any?) -> Void)? = nil) { - AAExecutions.execute(command, type: .Hidden, successBlock: successBlock, failureBlock: nil) + public func executeHidden(_ command: ACCommand, successBlock: ((_ val: Any?) -> Void)? = nil) { + AAExecutions.execute(command, type: .hidden, successBlock: successBlock, failureBlock: nil) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/ManagedAlerts.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/ManagedAlerts.swift index 7da126bc71..bb19fb9792 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/ManagedAlerts.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/ManagedAlerts.swift @@ -6,51 +6,51 @@ import Foundation public extension UIViewController { - public func alertSheet(@noescape closure: (a: AAAlertSetting) -> ()) { + public func alertSheet(_ closure: (_ a: AAAlertSetting) -> ()) { let s = AAAlertSetting() - closure(a: s) + closure(s) - let controller = UIAlertController(title: AALocalized(s.title), message: AALocalized(s.message), preferredStyle: .ActionSheet) + let controller = UIAlertController(title: AALocalized(s.title), message: AALocalized(s.message), preferredStyle: .actionSheet) for i in s.actions { - controller.addAction(UIAlertAction(title: AALocalized(i.title), style: i.isDestructive ? UIAlertActionStyle.Destructive : UIAlertActionStyle.Default, handler: { (c) -> Void in + controller.addAction(UIAlertAction(title: AALocalized(i.title), style: i.isDestructive ? UIAlertActionStyle.destructive : UIAlertActionStyle.default, handler: { (c) -> Void in i.closure() })) } - controller.addAction(UIAlertAction(title: AALocalized("AlertCancel"), style: .Cancel, handler: nil)) + controller.addAction(UIAlertAction(title: AALocalized("AlertCancel"), style: .cancel, handler: nil)) - presentViewController(controller, animated: true, completion: nil) + present(controller, animated: true, completion: nil) } - public func confirmDestructive(message: String, action: String, yes: ()->()) { - let controller = UIAlertController(title: nil, message: message, preferredStyle: .Alert) - controller.addAction(UIAlertAction(title: action, style: .Destructive, handler: { (act) -> Void in + public func confirmDestructive(_ message: String, action: String, yes: @escaping ()->()) { + let controller = UIAlertController(title: nil, message: message, preferredStyle: .alert) + controller.addAction(UIAlertAction(title: action, style: .destructive, handler: { (act) -> Void in yes() })) - controller.addAction(UIAlertAction(title: AALocalized("AlertCancel"), style: .Cancel, handler: nil)) - presentViewController(controller, animated: true, completion: nil) + controller.addAction(UIAlertAction(title: AALocalized("AlertCancel"), style: .cancel, handler: nil)) + present(controller, animated: true, completion: nil) } } -public class AAAlertSetting { +open class AAAlertSetting { - public var cancel: String! - public var title: String! - public var message: String! + open var cancel: String! + open var title: String! + open var message: String! - private var actions = [AlertActions]() + fileprivate var actions = [AlertActions]() - public func action(title: String, closure: ()->()) { + open func action(_ title: String, closure: @escaping ()->()) { let a = AlertActions() a.title = title a.closure = closure actions.append(a) } - public func destructive(title: String, closure: ()->()) { + open func destructive(_ title: String, closure: @escaping ()->()) { let a = AlertActions() a.title = title a.closure = closure @@ -63,4 +63,4 @@ class AlertActions { var isDestructive = false var title: String! var closure: (()->())! -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/ManagedBindedCells.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/ManagedBindedCells.swift index acd096c092..2e52b1c5cb 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/ManagedBindedCells.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/ManagedBindedCells.swift @@ -8,108 +8,108 @@ public protocol AABindedCell { associatedtype BindData - static func bindedCellHeight(table: AAManagedTable, item: BindData) -> CGFloat + static func bindedCellHeight(_ table: AAManagedTable, item: BindData) -> CGFloat - func bind(item: BindData, table: AAManagedTable, index: Int, totalCount: Int) + func bind(_ item: BindData, table: AAManagedTable, index: Int, totalCount: Int) } public protocol AABindedSearchCell { associatedtype BindData - static func bindedCellHeight(item: BindData) -> CGFloat + static func bindedCellHeight(_ item: BindData) -> CGFloat - func bind(item: BindData, search: String?) + func bind(_ item: BindData, search: String?) } -public class AABindedRows: NSObject, AAManagedRange, ARDisplayList_AppleChangeListener, ARDisplayList_Listener { +open class AABindedRows: NSObject, AAManagedRange, ARDisplayList_AppleChangeListener, ARDisplayList_Listener where BindCell: UITableViewCell, BindCell: AABindedCell { - public var topOffset: Int = 0 + open var topOffset: Int = 0 - public var displayList: ARBindedDisplayList! + open var displayList: ARBindedDisplayList! - public var selectAction: ((BindCell.BindData) -> Bool)? + open var selectAction: ((BindCell.BindData) -> Bool)? - public var canEditAction: ((BindCell.BindData) -> Bool)? + open var canEditAction: ((BindCell.BindData) -> Bool)? - public var editAction: ((BindCell.BindData) -> ())? + open var editAction: ((BindCell.BindData) -> ())? - public var didBind: ((BindCell, BindCell.BindData) -> ())? + open var didBind: ((BindCell, BindCell.BindData) -> ())? - public var autoHide = true + open var autoHide = true - public var animated = false + open var animated = false - public var differental = false + open var differental = false - private var table: AAManagedTable! + fileprivate var table: AAManagedTable! - private var lastItemsCount: Int = 0 + fileprivate var lastItemsCount: Int = 0 - private let cellReuseId = "Bind:\(BindCell.self)" + fileprivate let cellReuseId = "Bind:\(BindCell.self)" // Initing Table - public func initTable(table: AAManagedTable) { - table.tableView.registerClass(BindCell.self, forCellReuseIdentifier: cellReuseId) + open func initTable(_ table: AAManagedTable) { + table.tableView.register(BindCell.self, forCellReuseIdentifier: cellReuseId) } // Total items count - public func rangeNumberOfItems(table: AAManagedTable) -> Int { + open func rangeNumberOfItems(_ table: AAManagedTable) -> Int { return lastItemsCount } // Cells - public func rangeCellHeightForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { - let data = displayList.itemWithIndex(jint(indexPath.item)) as! BindCell.BindData + open func rangeCellHeightForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { + let data = displayList.item(with: jint(indexPath.item)) as! BindCell.BindData return BindCell.self.bindedCellHeight(table, item: data) } - public func rangeCellForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { - let data = displayList.itemWithIndex(jint(indexPath.item)) as! BindCell.BindData - let cell = self.table.tableView.dequeueReusableCellWithIdentifier(cellReuseId, forIndexPath: indexPath.indexPath) as! BindCell + open func rangeCellForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { + let data = displayList.item(with: jint(indexPath.item)) as! BindCell.BindData + let cell = self.table.tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath.indexPath as IndexPath) as! BindCell cell.bind(data, table: table, index: indexPath.item, totalCount: lastItemsCount) - displayList.touchWithIndex(jint(indexPath.item)) + displayList.touch(with: jint(indexPath.item)) didBind?(cell, data) return cell } // Select - public func rangeCanSelect(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { + open func rangeCanSelect(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { return selectAction != nil } - public func rangeSelect(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { + open func rangeSelect(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { - return selectAction!(displayList.itemWithIndex(jint(indexPath.item)) as! BindCell.BindData) + return selectAction!(displayList.item(with: jint(indexPath.item)) as! BindCell.BindData) } // Delete - public func rangeCanDelete(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { + open func rangeCanDelete(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { if canEditAction != nil { - return canEditAction!(displayList.itemWithIndex(jint(indexPath.item)) as! BindCell.BindData) + return canEditAction!(displayList.item(with: jint(indexPath.item)) as! BindCell.BindData) } return false } - public func rangeDelete(table: AAManagedTable, indexPath: AARangeIndexPath) { - editAction!(displayList.itemWithIndex(jint(indexPath.item)) as! BindCell.BindData) + open func rangeDelete(_ table: AAManagedTable, indexPath: AARangeIndexPath) { + editAction!(displayList.item(with: jint(indexPath.item)) as! BindCell.BindData) } // Binding - public func rangeBind(table: AAManagedTable, binder: AABinder) { + open func rangeBind(_ table: AAManagedTable, binder: AABinder) { self.table = table if differental { displayList.addAppleListener(self) } else { - displayList.addListener(self) + displayList.add(self) } lastItemsCount = Int(displayList.size()) @@ -117,7 +117,7 @@ public class AABindedRows= topOffset { - let data = displayList.itemWithIndex(jint(index.row - topOffset)) as! BindCell.BindData - (cell as! BindCell).bind(data, table: table, index: index.row - topOffset, totalCount: Int(displayList.size())) + if (index as NSIndexPath).section == 0 && (index as NSIndexPath).row >= topOffset { + let data = displayList.item(with: jint((index as NSIndexPath).row - topOffset)) as! BindCell.BindData + (cell as! BindCell).bind(data, table: table, index: (index as NSIndexPath).row - topOffset, totalCount: Int(displayList.size())) } } } @@ -143,7 +143,7 @@ public class AABindedRows 0 { @@ -170,27 +170,27 @@ public class AABindedRows 0 { - var rows = [NSIndexPath]() + var rows = [IndexPath]() for i in 0.. 0 { - var rows = [NSIndexPath]() + var rows = [IndexPath]() for i in 0.. 0 { for i in 0..= topOffset { - let data = displayList.itemWithIndex(jint(index.row - topOffset)) as! BindCell.BindData - (cell as! BindCell).bind(data, table: table, index: index.row - topOffset, totalCount: Int(displayList.size())) + if (index as NSIndexPath).section == 0 && (index as NSIndexPath).row >= topOffset { + let data = displayList.item(with: jint((index as NSIndexPath).row - topOffset)) as! BindCell.BindData + (cell as! BindCell).bind(data, table: table, index: (index as NSIndexPath).row - topOffset, totalCount: Int(displayList.size())) } } } @@ -244,7 +244,7 @@ public class AABindedRows(@noescape closure: (r: AABindedRows) -> ()) -> AABindedRows { + public func binded(_ closure: (_ r: AABindedRows) -> ()) -> AABindedRows where T: UITableViewCell, T: AABindedCell { let topOffset = numberOfItems(self.table) let r = AABindedRows() r.topOffset = topOffset regions.append(r) - closure(r: r) + closure(r) r.checkInstallation() r.initTable(self.table) return r diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/ManagedCells.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/ManagedCells.swift index 5f3c160e5f..c96c03b6d3 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/ManagedCells.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/ManagedCells.swift @@ -6,20 +6,21 @@ import Foundation // Edit Row -public class AAEditRow: AAManagedRow, UITextFieldDelegate { +open class AAEditRow: AAManagedRow, UITextFieldDelegate { - public var text: String? - public var placeholder: String? - public var returnKeyType = UIReturnKeyType.Default - public var autocorrectionType = UITextAutocorrectionType.Default - public var autocapitalizationType = UITextAutocapitalizationType.Sentences - public var returnAction: (()->())? + open var prefix: String? + open var text: String? + open var placeholder: String? + open var returnKeyType = UIReturnKeyType.default + open var autocorrectionType = UITextAutocorrectionType.default + open var autocapitalizationType = UITextAutocapitalizationType.sentences + open var returnAction: (()->())? - public override func rangeCellHeightForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { + open override func rangeCellHeightForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { return 44 } - public override func rangeCellForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { + open override func rangeCellForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { let res = table.dequeueCell(indexPath.indexPath) as AAEditCell res.textField.text = text @@ -34,20 +35,28 @@ public class AAEditRow: AAManagedRow, UITextFieldDelegate { res.textField.returnKeyType = returnKeyType res.textField.autocapitalizationType = autocapitalizationType res.textField.delegate = self - res.textField.removeTarget(nil, action: nil, forControlEvents: .AllEvents) - res.textField.addTarget(self, action: #selector(AAEditRow.textFieldDidChange(_:)), forControlEvents: .EditingChanged) + res.textField.removeTarget(nil, action: nil, for: .allEvents) + res.textField.addTarget(self, action: #selector(AAEditRow.textFieldDidChange(_:)), for: .editingChanged) + + if prefix != nil { + res.textPrefix.text = prefix + res.textPrefix.isHidden = false + } else { + res.textPrefix.isHidden = true + } + return res } - public func textFieldDidChange(textField: UITextField) { + open func textFieldDidChange(_ textField: UITextField) { text = textField.text } - @objc public func textFieldDidEndEditing(textField: UITextField) { + @objc open func textFieldDidEndEditing(_ textField: UITextField) { text = textField.text } - public func textFieldShouldReturn(textField: UITextField) -> Bool { + open func textFieldShouldReturn(_ textField: UITextField) -> Bool { returnAction?() return false } @@ -55,10 +64,10 @@ public class AAEditRow: AAManagedRow, UITextFieldDelegate { public extension AAManagedSection { - public func edit(@noescape closure: (r: AAEditRow) -> ()) -> AAEditRow { + public func edit(_ closure: (_ r: AAEditRow) -> ()) -> AAEditRow { let r = AAEditRow() regions.append(r) - closure(r: r) + closure(r) r.initTable(self.table) return r } @@ -66,29 +75,29 @@ public extension AAManagedSection { // Titled Row -public class AATitledRow: AAManagedRow { +open class AATitledRow: AAManagedRow { - public var title: String? - public var subtitle: String? + open var title: String? + open var subtitle: String? - public var isAction: Bool = false - public var accessoryType = UITableViewCellAccessoryType.None + open var isAction: Bool = false + open var accessoryType = UITableViewCellAccessoryType.none - public var bindAction: ((r: AATitledRow)->())? + open var bindAction: ((_ r: AATitledRow)->())? // Cell - public override func rangeCellHeightForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { + open override func rangeCellHeightForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { return 55 } - public override func rangeCellForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { + open override func rangeCellForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { let res = table.dequeueTitledCell(indexPath.indexPath) bindCell(res) return res } - public func bindCell(res: AATitledCell) { + open func bindCell(_ res: AATitledCell) { res.titleLabel.text = title res.contentLabel.text = subtitle res.accessoryType = accessoryType @@ -103,18 +112,18 @@ public class AATitledRow: AAManagedRow { // Copy - public override func rangeCopyData(table: AAManagedTable, indexPath: AARangeIndexPath) -> String? { + open override func rangeCopyData(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> String? { return isAction ? nil : subtitle } // Binding - public override func reload() { + open override func reload() { - bindAction?(r: self) + bindAction?(self) if let p = indexPath, let s = section { - if let cell = s.table.tableView.cellForRowAtIndexPath(p) as? AATitledCell { + if let cell = s.table.tableView.cellForRow(at: p) as? AATitledCell { bindCell(cell) } } @@ -123,32 +132,32 @@ public class AATitledRow: AAManagedRow { public extension AAManagedSection { - private func titled() -> AATitledRow { + fileprivate func titled() -> AATitledRow { let r = AATitledRow() let itemsCount = numberOfItems(table) regions.append(r) - r.indexPath = NSIndexPath(forRow: itemsCount, inSection: index) + r.indexPath = IndexPath(row: itemsCount, section: index) r.section = self r.initTable(self.table) return r } - public func titled(@noescape closure: (r: AATitledRow) -> ()) -> AATitledRow { + public func titled(_ closure: (_ r: AATitledRow) -> ()) -> AATitledRow { let r = titled() - closure(r: r) + closure(r) r.initTable(self.table) return r } - public func titled(title: String, @noescape closure: (r: AATitledRow) -> ()) -> AATitledRow { + public func titled(_ title: String, closure: (_ r: AATitledRow) -> ()) -> AATitledRow { let r = titled() r.title = AALocalized(title) - closure(r: r) + closure(r) r.initTable(self.table) return r } - public func titled(title: String, content: String) -> AATitledRow { + public func titled(_ title: String, content: String) -> AATitledRow { let r = titled() r.title = AALocalized(title) r.subtitle = content @@ -159,44 +168,44 @@ public extension AAManagedSection { // Text Row -public class AATextRow: AAManagedRow { +open class AATextRow: AAManagedRow { - public var title: String? - public var content: String? + open var title: String? + open var content: String? - public var isAction: Bool = false - public var navigate: Bool = false + open var isAction: Bool = false + open var navigate: Bool = false - public var bindAction: ((r: AATextRow)->())? + open var bindAction: ((_ r: AATextRow)->())? // Cell - public override func rangeCellHeightForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { - return AATextCell.measure(content!, width: table.tableView.width, enableNavigation: navigate) + open override func rangeCellHeightForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { + return AATextCell.measure(title, text: content!, width: table.tableView.width, enableNavigation: navigate) } - public override func rangeCellForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { + open override func rangeCellForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { let res = table.dequeueTextCell(indexPath.indexPath) res.setContent(title, content: content, isAction: isAction) if navigate { - res.accessoryType = UITableViewCellAccessoryType.DisclosureIndicator + res.accessoryType = UITableViewCellAccessoryType.disclosureIndicator } else { - res.accessoryType = UITableViewCellAccessoryType.None + res.accessoryType = UITableViewCellAccessoryType.none } return res } // Copy - public override func rangeCopyData(table: AAManagedTable, indexPath: AARangeIndexPath) -> String? { + open override func rangeCopyData(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> String? { return isAction ? nil : content } // Binding - public override func reload() { + open override func reload() { - bindAction?(r: self) + bindAction?(self) super.reload() } @@ -204,36 +213,48 @@ public class AATextRow: AAManagedRow { public extension AAManagedSection { - private func text() -> AATextRow { + fileprivate func text() -> AATextRow { let r = AATextRow() let itemsCount = numberOfItems(table) regions.append(r) - r.indexPath = NSIndexPath(forRow: itemsCount, inSection: index) + r.indexPath = IndexPath(row: itemsCount, section: index) r.section = self r.initTable(self.table) return r } - public func text(@noescape closure: (r: AATextRow) -> ()) -> AATextRow { + public func text(_ closure: (_ r: AATextRow) -> ()) -> AATextRow { let r = text() - closure(r: r) - r.bindAction?(r: r) + closure(r) + r.bindAction?(r) r.initTable(self.table) return r } - public func text(title: String, @noescape closure: (r: AATextRow) -> ()) -> AATextRow { + public func text(_ title: String?, closure: (_ r: AATextRow) -> ()) -> AATextRow { let r = text() - r.title = AALocalized(title) - closure(r: r) - r.bindAction?(r: r) + if title != nil { + r.title = AALocalized(title!) + } + closure(r) + r.bindAction?(r) r.initTable(self.table) return r } - public func text(title: String, content: String) -> AATextRow { + public func text(_ title: String?, content: String) -> AATextRow { let r = text() - r.title = AALocalized(title) + if title != nil { + r.title = AALocalized(title!) + } + r.content = content + r.initTable(self.table) + return r + } + + public func text(_ content: String) -> AATextRow { + let r = text() + r.title = nil r.content = content r.initTable(self.table) return r @@ -243,30 +264,30 @@ public extension AAManagedSection { // Header Row -public class AAHeaderRow: AAManagedRow { +open class AAHeaderRow: AAManagedRow { - public var height: CGFloat = 40 - public var title: String? - public var icon: UIImage? + open var height: CGFloat = 40 + open var title: String? + open var icon: UIImage? - public override func rangeCellHeightForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { + open override func rangeCellHeightForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { return height } - public override func rangeCellForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { + open override func rangeCellForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { let res = table.dequeueCell(indexPath.indexPath) as AAHeaderCell if title == nil { - res.titleView.hidden = true + res.titleView.isHidden = true } else { res.titleView.text = title - res.titleView.hidden = false + res.titleView.isHidden = false } if icon == nil { - res.iconView.hidden = true + res.iconView.isHidden = true } else { - res.iconView.hidden = false + res.iconView.isHidden = false res.iconView.image = icon!.tintImage(ActorSDK.sharedActor().style.cellHeaderColor) } @@ -276,7 +297,7 @@ public class AAHeaderRow: AAManagedRow { public extension AAManagedSection { - private func header() -> AAHeaderRow { + fileprivate func header() -> AAHeaderRow { let r = AAHeaderRow() regions.append(r) r.section = self @@ -284,7 +305,7 @@ public extension AAManagedSection { return r } - public func header(title: String) -> AAHeaderRow { + public func header(_ title: String) -> AAHeaderRow { let r = header() r.title = title r.initTable(self.table) @@ -295,31 +316,31 @@ public extension AAManagedSection { // Common Row -public class AACommonRow: AAManagedRow { +open class AACommonRow: AAManagedRow { - public var style: AACommonCellStyle = .Normal - public var hint: String? - public var content: String? - public var switchOn: Bool = false - public var switchAction: ((v: Bool) -> ())? + open var style: AACommonCellStyle = .normal + open var hint: String? + open var content: String? + open var switchOn: Bool = false + open var switchAction: ((_ v: Bool) -> ())? - public var bindAction: ((r: AACommonRow)->())? + open var bindAction: ((_ r: AACommonRow)->())? - public var contentInset: CGFloat = 15 + open var contentInset: CGFloat = 15 // Cell - public override func rangeCellHeightForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { + open override func rangeCellHeightForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { return 44 } - public override func rangeCellForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { + open override func rangeCellForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { let res = table.dequeueCommonCell(indexPath.indexPath) bindCell(res) return res } - public func bindCell(res: AACommonCell) { + open func bindCell(_ res: AACommonCell) { res.style = style res.setContent(content) res.setHint(hint) @@ -328,124 +349,132 @@ public class AACommonRow: AAManagedRow { res.contentInset = contentInset if selectAction != nil { - res.selectionStyle = .Default + res.selectionStyle = .default } else { - res.selectionStyle = .None + res.selectionStyle = .none } } // Binding - public override func reload() { + open func rangeBind(_ table: AAManagedTable, binder: AABinder) { + bindAction?(self) + } + + open override func reload() { - bindAction?(r: self) + bindAction?(self) if let p = indexPath, let s = section { - if let cell = s.table.tableView.cellForRowAtIndexPath(p) as? AACommonCell { + if let cell = s.table.tableView.cellForRow(at: p) as? AACommonCell { bindCell(cell) } } } + + open func rebind() { + bindAction?(self) + } } public extension AAManagedSection { // Common cell - private func common() -> AACommonRow { + fileprivate func common() -> AACommonRow { let r = AACommonRow() let itemsCount = numberOfItems(table) regions.append(r) - r.indexPath = NSIndexPath(forRow: itemsCount, inSection: index) + r.indexPath = IndexPath(row: itemsCount, section: index) r.section = self r.initTable(self.table) return r } - public func common(@noescape closure: (r: AACommonRow) -> ()) -> AACommonRow { + public func common(_ closure: (_ r: AACommonRow) -> ()) -> AACommonRow { let r = common() - closure(r: r) - r.bindAction?(r: r) + closure(r) + r.bindAction?(r) r.initTable(self.table) return r } // Action cell - public func action(@noescape closure: (r: AACommonRow) -> ()) -> AACommonRow { + public func action(_ closure: (_ r: AACommonRow) -> ()) -> AACommonRow { let r = common() - r.style = .Action - closure(r: r) - r.bindAction?(r: r) + r.style = .action + closure(r) + r.bindAction?(r) r.initTable(self.table) return r } - public func action(content: String, @noescape closure: (r: AACommonRow) -> ()) -> AACommonRow { + public func action(_ content: String, closure: (_ r: AACommonRow) -> ()) -> AACommonRow { let r = common() r.content = AALocalized(content) - r.style = .Action - closure(r: r) - r.bindAction?(r: r) + r.style = .action + closure(r) + r.bindAction?(r) r.initTable(self.table) return r } // Navigation cell - public func navigate(@noescape closure: (r: AACommonRow) -> ()) -> AACommonRow { + public func navigate(_ closure: (_ r: AACommonRow) -> ()) -> AACommonRow { let r = common() - r.style = .Navigation - closure(r: r) - r.bindAction?(r: r) + r.style = .navigation + closure(r) + r.bindAction?(r) r.initTable(self.table) return r } - public func navigate(content: String, @noescape closure: (r: AACommonRow) -> ()) -> AACommonRow { + public func navigate(_ content: String, closure: (_ r: AACommonRow) -> ()) -> AACommonRow { let r = common() r.content = AALocalized(content) - r.style = .Navigation - closure(r: r) - r.bindAction?(r: r) + r.style = .navigation + closure(r) + r.bindAction?(r) r.initTable(self.table) return r } - public func navigate(content: String, controller: UIViewController.Type) -> AACommonRow { + public func navigate(_ content: String, controller: UIViewController.Type) -> AACommonRow { let r = common() r.content = AALocalized(content) - r.style = .Navigation + r.style = .navigation r.selectAction = { () -> Bool in self.table.controller.navigateNext(controller.init()) return false } - r.bindAction?(r: r) + r.bindAction?(r) r.initTable(self.table) return r } // Danger - public func danger(content: String, @noescape closure: (r: AACommonRow) -> ()) -> AACommonRow { + public func danger(_ content: String, closure: (_ r: AACommonRow) -> ()) -> AACommonRow { let r = common() r.content = AALocalized(content) - r.style = .Destructive - closure(r: r) - r.bindAction?(r: r) + r.style = .destructive + closure(r) + r.bindAction?(r) r.initTable(self.table) return r } // Content - func url(content: String, url: String) -> AACommonRow { + func url(_ content: String, url: String) -> AACommonRow { let r = common() r.content = AALocalized(content) - r.style = .Navigation + r.style = .navigation r.selectAction = { () -> Bool in - if let u = NSURL(string: url) { - UIApplication.sharedApplication().openURL(u) + if let u = URL(string: url) { + UIApplication.shared.openURL(u) } return true } @@ -453,11 +482,11 @@ public extension AAManagedSection { return r } - public func hint(content: String) -> AACommonRow { + public func hint(_ content: String) -> AACommonRow { let r = common() r.content = AALocalized(content) - r.style = .Hint - r.bindAction?(r: r) + r.style = .hint + r.bindAction?(r) r.initTable(self.table) return r } @@ -466,34 +495,34 @@ public extension AAManagedSection { // Custom cell -public class AACustomRow: AAManagedRow { +open class AACustomRow: AAManagedRow where T: UITableViewCell { - public var height: CGFloat = 44 - public var closure: ((cell: T) -> ())? + open var height: CGFloat = 44 + open var closure: ((_ cell: T) -> ())? - public var bindAction: ((r: AACustomRow)->())? + open var bindAction: ((_ r: AACustomRow)->())? // Cell - public override func rangeCellHeightForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { + open override func rangeCellHeightForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { return height } - public override func rangeCellForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { + open override func rangeCellForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { let res: T = table.dequeueCell(indexPath.indexPath) rangeBindCellForItem(table, indexPath: indexPath, cell: res) return res } - public func rangeBindCellForItem(table: AAManagedTable, indexPath: AARangeIndexPath, cell: T) { - closure?(cell: cell) + open func rangeBindCellForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath, cell: T) { + closure?(cell) } // Binding - public override func reload() { + open override func reload() { - bindAction?(r: self) + bindAction?(self) super.reload() } @@ -501,20 +530,20 @@ public class AACustomRow: AAManagedRow { public extension AAManagedSection { - private func custom() -> AACustomRow { + fileprivate func custom() -> AACustomRow where T: UITableViewCell { let r = AACustomRow() let itemsCount = numberOfItems(table) regions.append(r) - r.indexPath = NSIndexPath(forRow: itemsCount, inSection: index) + r.indexPath = IndexPath(row: itemsCount, section: index) r.section = self r.initTable(self.table) return r } - public func custom(@noescape closure: (r: AACustomRow) -> ()) -> AACustomRow { + public func custom(_ closure: (_ r: AACustomRow) -> ()) -> AACustomRow where T: UITableViewCell { let r: AACustomRow = custom() - closure(r: r) - r.bindAction?(r: r) + closure(r) + r.bindAction?(r) r.initTable(self.table) return r } @@ -522,39 +551,39 @@ public extension AAManagedSection { // Avatar Row -public class AAAvatarRow: AAManagedRow { +open class AAAvatarRow: AAManagedRow { - public var id: Int? - public var title: String? + open var id: Int? + open var title: String? - public var avatar: ACAvatar? - public var avatarPath: String? - public var avatarLoading: Bool = false + open var avatar: ACAvatar? + open var avatarPath: String? + open var avatarLoading: Bool = false - public var subtitleHidden: Bool = false - public var subtitle: String? - public var subtitleColor: UIColor? + open var subtitleHidden: Bool = false + open var subtitle: String? + open var subtitleColor: UIColor? - public var bindAction: ((r: AAAvatarRow)->())? + open var bindAction: ((_ r: AAAvatarRow)->())? - public var avatarDidTap: ((view: UIView) -> ())? + open var avatarDidTap: ((_ view: UIView) -> ())? // Cell - public override func rangeCellHeightForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { + open override func rangeCellHeightForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { return 92 } - public override func rangeCellForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { + open override func rangeCellForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { let res: AAAvatarCell = table.dequeueCell(indexPath.indexPath) bindCell(res) return res } - public func bindCell(res: AAAvatarCell) { + open func bindCell(_ res: AAAvatarCell) { res.titleLabel.text = title - res.subtitleLabel.hidden = subtitleHidden + res.subtitleLabel.isHidden = subtitleHidden res.subtitleLabel.text = subtitle if avatarPath != nil { @@ -569,7 +598,7 @@ public class AAAvatarRow: AAManagedRow { res.subtitleLabel.textColor = ActorSDK.sharedActor().style.cellTextColor } - res.progress.hidden = !avatarLoading + res.progress.isHidden = !avatarLoading if avatarLoading { res.progress.startAnimating() } else { @@ -581,9 +610,9 @@ public class AAAvatarRow: AAManagedRow { // Binding - public override func reload() { + open override func reload() { - bindAction?(r: self) + bindAction?(self) if let p = indexPath, let s = section { if let cell = s.table.tableView.visibleCellForIndexPath(p) as? AAAvatarCell { @@ -595,20 +624,20 @@ public class AAAvatarRow: AAManagedRow { public extension AAManagedSection { - private func addAvatar() -> AAAvatarRow { + fileprivate func addAvatar() -> AAAvatarRow { let r = AAAvatarRow() let itemsCount = numberOfItems(table) regions.append(r) - r.indexPath = NSIndexPath(forRow: itemsCount, inSection: index) + r.indexPath = IndexPath(row: itemsCount, section: index) r.section = self r.initTable(self.table) return r } - public func avatar(@noescape closure: (r: AAAvatarRow) -> ()) -> AAAvatarRow { + public func avatar(_ closure: (_ r: AAAvatarRow) -> ()) -> AAAvatarRow { let r: AAAvatarRow = addAvatar() - closure(r: r) - r.bindAction?(r: r) + closure(r) + r.bindAction?(r) r.initTable(self.table) return r } @@ -616,80 +645,88 @@ public extension AAManagedSection { // Arrays -public class AAManagedArrayRows: AAManagedRange { +open class AAManagedArrayRows: AAManagedRange where R: UITableViewCell { - public var height: CGFloat = 44 - public var selectAction: ((t: T) -> Bool)? - public var bindCopy: ((t: T) -> String?)? - public var section: AAManagedSection? + open var height: CGFloat = 44 + open var selectAction: ((_ t: T) -> Bool)? + open var bindCopy: ((_ t: T) -> String?)? + open var section: AAManagedSection? - public var bindData: ((cell: R, item: T) -> ())? + open var bindData: ((_ cell: R, _ item: T) -> ())? - public var data = [T]() + open var itemShown: ((_ index: Int, _ item: T) -> ())? - public func initTable(table: AAManagedTable) { + open var data = [T]() + + open func initTable(_ table: AAManagedTable) { } // Number of items - public func rangeNumberOfItems(table: AAManagedTable) -> Int { + open func rangeNumberOfItems(_ table: AAManagedTable) -> Int { return data.count } // Cells - public func rangeCellHeightForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { + open func rangeCellHeightForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { return height } - public func rangeCellForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { + open func rangeCellForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { let res: R = table.dequeueCell(indexPath.indexPath) - rangeBindData(table, indexPath: indexPath, cell: res, item: data[indexPath.item]) + let item = data[indexPath.item] + rangeBindData(table, indexPath: indexPath, cell: res, item: item) + + if let shown = itemShown { + shown(indexPath.item, item) + } + return res } - public func rangeBindData(table: AAManagedTable, indexPath: AARangeIndexPath, cell: R, item: T) { - bindData?(cell: cell, item: item) + open func rangeBindData(_ table: AAManagedTable, indexPath: AARangeIndexPath, cell: R, item: T) { + bindData?(cell, item) } // Selection - public func rangeCanSelect(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { + open func rangeCanSelect(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { return selectAction != nil } - public func rangeSelect(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { - return selectAction!(t: data[indexPath.item]) + open func rangeSelect(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { + return selectAction!(data[indexPath.item]) } // Copy - public func rangeCanCopy(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { + open func rangeCanCopy(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { if bindCopy != nil { - return bindCopy!(t: data[indexPath.item]) != nil + return bindCopy!(data[indexPath.item]) != nil } return false } - public func rangeCopy(table: AAManagedTable, indexPath: AARangeIndexPath) { - if let s = bindCopy!(t: data[indexPath.item]) { - UIPasteboard.generalPasteboard().string = s + open func rangeCopy(_ table: AAManagedTable, indexPath: AARangeIndexPath) { + if let s = bindCopy!(data[indexPath.item]) { + UIPasteboard.general.string = s } } // Reloading - public func reload() { + open func reload() { if let s = section { - s.table.tableView.reloadSections(NSIndexSet(index: s.index), withRowAnimation: .Automatic) + s.table.tableView.reloadSections(IndexSet(integer: s.index), with: .automatic) } } } public extension AAManagedSection { - private func addArrays() -> AAManagedArrayRows { + fileprivate func addArrays() -> AAManagedArrayRows where R: UITableViewCell { let r = AAManagedArrayRows() regions.append(r) r.section = self @@ -697,9 +734,9 @@ public extension AAManagedSection { return r } - public func arrays(@noescape closure: (r: AAManagedArrayRows) -> ()) -> AAManagedArrayRows { + public func arrays(_ closure: (_ r: AAManagedArrayRows) -> ()) -> AAManagedArrayRows where R: UITableViewCell { let res: AAManagedArrayRows = addArrays() - closure(r: res) + closure(res) res.initTable(self.table) return res } @@ -708,60 +745,60 @@ public extension AAManagedSection { // Single item row -public class AAManagedRow: NSObject, AAManagedRange { +open class AAManagedRow: NSObject, AAManagedRange { - public var selectAction: (() -> Bool)? + open var selectAction: (() -> Bool)? - public var section: AAManagedSection? - public var indexPath: NSIndexPath? + open var section: AAManagedSection? + open var indexPath: IndexPath? - public func initTable(table: AAManagedTable) { + open func initTable(_ table: AAManagedTable) { } // Number of items - public func rangeNumberOfItems(table: AAManagedTable) -> Int { + open func rangeNumberOfItems(_ table: AAManagedTable) -> Int { return 1 } // Cells - public func rangeCellHeightForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { + open func rangeCellHeightForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> CGFloat { return 44 } - public func rangeCellForItem(table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { + open func rangeCellForItem(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> UITableViewCell { fatalError("Not implemented") } // Selection - public func rangeCanSelect(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { + open func rangeCanSelect(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { return selectAction != nil } - public func rangeSelect(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { + open func rangeSelect(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { return selectAction!() } // Copying - public func rangeCanCopy(table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { + open func rangeCanCopy(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> Bool { return rangeCopyData(table, indexPath: indexPath) != nil } - public func rangeCopy(table: AAManagedTable, indexPath: AARangeIndexPath) { - UIPasteboard.generalPasteboard().string = rangeCopyData(table, indexPath: indexPath) + open func rangeCopy(_ table: AAManagedTable, indexPath: AARangeIndexPath) { + UIPasteboard.general.string = rangeCopyData(table, indexPath: indexPath) } - public func rangeCopyData(table: AAManagedTable, indexPath: AARangeIndexPath) -> String? { + open func rangeCopyData(_ table: AAManagedTable, indexPath: AARangeIndexPath) -> String? { return nil } - public func reload() { + open func reload() { if let p = indexPath, let s = section { - s.table.tableView.reloadRowsAtIndexPaths([p], withRowAnimation: .Automatic) + s.table.tableView.reloadRows(at: [p], with: .automatic) } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/ManagedTableExtensions.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/ManagedTableExtensions.swift index 2aa8ab8ac0..619003edd8 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/ManagedTableExtensions.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/ManagedTableExtensions.swift @@ -6,22 +6,22 @@ import Foundation public extension AAManagedTable { - public func dequeueCell(indexPath: NSIndexPath) -> T { + public func dequeueCell(_ indexPath: IndexPath) -> T where T: UITableViewCell { return self.tableView.dequeueCell(indexPath) } } public extension AAManagedTable { - public func dequeueTextCell(indexPath: NSIndexPath) -> AATextCell { + public func dequeueTextCell(_ indexPath: IndexPath) -> AATextCell { return dequeueCell(indexPath) } - public func dequeueTitledCell(indexPath: NSIndexPath) -> AATitledCell { + public func dequeueTitledCell(_ indexPath: IndexPath) -> AATitledCell { return dequeueCell(indexPath) } - public func dequeueCommonCell(indexPath: NSIndexPath) -> AACommonCell { + public func dequeueCommonCell(_ indexPath: IndexPath) -> AACommonCell { return dequeueCell(indexPath) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/Navigations.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/Navigations.swift index 6003e7955e..88c83b56e0 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/Navigations.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Managed Runtime/Navigations.swift @@ -5,15 +5,15 @@ import Foundation public extension UIViewController { - public func navigateDetail(controller: UIViewController) { + public func navigateDetail(_ controller: UIViewController) { if (AADevice.isiPad) { - let split = UIApplication.sharedApplication().keyWindow?.rootViewController as! UISplitViewController + let split = UIApplication.shared.keyWindow?.rootViewController as! UISplitViewController let master = split.viewControllers[0] let detail = AANavigationController() detail.viewControllers = [controller] split.viewControllers = [master, detail] } else { - let tabBar = UIApplication.sharedApplication().keyWindow?.rootViewController as! UITabBarController + let tabBar = UIApplication.shared.keyWindow?.rootViewController as! UITabBarController controller.hidesBottomBarWhenPushed = true (tabBar.selectedViewController as! AANavigationController).pushViewController(controller, animated: true) } @@ -21,7 +21,7 @@ public extension UIViewController { } public extension UIViewController { - public func navigateNext(controller: UIViewController, removeCurrent: Bool = false) { + public func navigateNext(_ controller: UIViewController, removeCurrent: Bool = false) { if let aaC = controller as? AAViewController, let aaSelf = self as? AAViewController { aaC.popover = aaSelf.popover } @@ -45,7 +45,7 @@ public extension UIViewController { public func navigateBack() { if (self.navigationController!.viewControllers.last != nil) { if (self.navigationController!.viewControllers.last! == self) { - self.navigationController!.popViewControllerAnimated(true) + self.navigationController!.popViewController(animated: true) } else { } @@ -60,4 +60,4 @@ public extension UIViewController { self.navigationController!.setViewControllers(nControllers, animated: true); } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Recent/AARecentViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Recent/AARecentViewController.swift index b7a21d736f..2138b077d4 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Recent/AARecentViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Recent/AARecentViewController.swift @@ -4,9 +4,9 @@ import UIKit -public class AARecentViewController: AADialogsListContentController, AADialogsListContentControllerDelegate { +open class AARecentViewController: AADialogsListContentController, AADialogsListContentControllerDelegate { - private var isBinded = true + fileprivate var isBinded = true public override init() { @@ -14,7 +14,7 @@ public class AARecentViewController: AADialogsListContentController, AADialogsLi // Enabling dialogs page tracking - content = ACAllEvents_Main.RECENT() + content = ACAllEvents_Main.recent() // Setting delegate @@ -27,10 +27,10 @@ public class AARecentViewController: AADialogsListContentController, AADialogsLi // Setting navigation item navigationItem.title = AALocalized("TabMessages") - navigationItem.leftBarButtonItem = editButtonItem() + navigationItem.leftBarButtonItem = editButtonItem navigationItem.leftBarButtonItem!.title = AALocalized("NavigationEdit") - navigationItem.backBarButtonItem = UIBarButtonItem(title: AALocalized("DialogsBack"), style: UIBarButtonItemStyle.Plain, target: nil, action: nil) - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Compose, target: self, action: #selector(AARecentViewController.compose)) + navigationItem.backBarButtonItem = UIBarButtonItem(title: AALocalized("DialogsBack"), style: UIBarButtonItemStyle.plain, target: nil, action: nil) + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.compose, target: self, action: #selector(AARecentViewController.compose)) bindCounter() } @@ -41,31 +41,30 @@ public class AARecentViewController: AADialogsListContentController, AADialogsLi // Implemention of editing - public override func setEditing(editing: Bool, animated: Bool) { + open override func setEditing(_ editing: Bool, animated: Bool) { super.setEditing(editing, animated: animated) tableView.setEditing(editing, animated: animated) if (editing) { self.navigationItem.leftBarButtonItem!.title = AALocalized("NavigationDone") - self.navigationItem.leftBarButtonItem!.style = UIBarButtonItemStyle.Done + self.navigationItem.leftBarButtonItem!.style = UIBarButtonItemStyle.done navigationItem.rightBarButtonItem = nil - } - else { + } else { self.navigationItem.leftBarButtonItem!.title = AALocalized("NavigationEdit") - self.navigationItem.leftBarButtonItem!.style = UIBarButtonItemStyle.Plain + self.navigationItem.leftBarButtonItem!.style = UIBarButtonItemStyle.plain - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Compose, target: self, action: #selector(AARecentViewController.compose)) + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.compose, target: self, action: #selector(AARecentViewController.compose)) } if editing == true { navigationItem.rightBarButtonItem = nil } else { - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Compose, target: self, action: #selector(AARecentViewController.compose)) + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.compose, target: self, action: #selector(AARecentViewController.compose)) } } - public func compose() { + open func compose() { if AADevice.isiPad { self.presentElegantViewController(AANavigationController(rootViewController: AAComposeController())) } else { @@ -75,7 +74,7 @@ public class AARecentViewController: AADialogsListContentController, AADialogsLi // Tracking app state - public override func viewWillAppear(animated: Bool) { + open override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) bindCounter() } @@ -85,8 +84,8 @@ public class AARecentViewController: AADialogsListContentController, AADialogsLi isBinded = true binder.bind(Actor.getGlobalState().globalCounter, closure: { (value: JavaLangInteger?) -> () in if value != nil { - if value!.integerValue > 0 { - self.tabBarItem.badgeValue = "\(value!.integerValue)" + if value!.intValue > 0 { + self.tabBarItem.badgeValue = "\(value!.intValue)" } else { self.tabBarItem.badgeValue = nil } @@ -99,24 +98,24 @@ public class AARecentViewController: AADialogsListContentController, AADialogsLi } - public override func viewWillDisappear(animated: Bool) { + open override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) isBinded = false } - public override func viewDidAppear(animated: Bool) { + open override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) Actor.onDialogsOpen() } - public override func viewDidDisappear(animated: Bool) { + open override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) Actor.onDialogsClosed() } // Handling selections - public func recentsDidTap(controller: AADialogsListContentController, dialog: ACDialog) -> Bool { + open func recentsDidTap(_ controller: AADialogsListContentController, dialog: ACDialog) -> Bool { if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(dialog.peer) { self.navigateDetail(customController) } else { @@ -125,7 +124,7 @@ public class AARecentViewController: AADialogsListContentController, AADialogsLi return false } - public func searchDidTap(controller: AADialogsListContentController, entity: ACSearchEntity) { + open func searchDidTap(_ controller: AADialogsListContentController, entity: ACSearchResult) { if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(entity.peer) { self.navigateDetail(customController) } else { diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Ringtones/AARingtonesViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Ringtones/AARingtonesViewController.swift index 121fe920d3..ecbdaef3f2 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Ringtones/AARingtonesViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Ringtones/AARingtonesViewController.swift @@ -10,7 +10,7 @@ import Foundation import UIKit import AVFoundation -public class AARingtonesViewController: AATableViewController { +open class AARingtonesViewController: AATableViewController { var audioPlayer: AVAudioPlayer! var selectedRingtone: String = "" @@ -21,21 +21,21 @@ public class AARingtonesViewController: AATableViewController { var soundFiles: [(directory: String, files: [String])] = [] init() { - super.init(style: UITableViewStyle.Plain) + super.init(style: UITableViewStyle.plain) self.title = AALocalized("Ringtones") - let cancelButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: UIBarButtonItemStyle.Plain, target: self, action: Selector("dismiss")) - let doneButtonItem = UIBarButtonItem(title: AALocalized("NavigationDone"), style: UIBarButtonItemStyle.Plain, target: self, action: Selector("dismiss")) - self.navigationItem.setLeftBarButtonItem(cancelButtonItem, animated: false) - self.navigationItem.setRightBarButtonItem(doneButtonItem, animated: false) + let cancelButtonItem = UIBarButtonItem(title: AALocalized("NavigationCancel"), style: UIBarButtonItemStyle.plain, target: self, action: Selector("dismiss")) + let doneButtonItem = UIBarButtonItem(title: AALocalized("NavigationDone"), style: UIBarButtonItemStyle.plain, target: self, action: Selector("dismiss")) + self.navigationItem.setLeftBarButton(cancelButtonItem, animated: false) + self.navigationItem.setRightBarButton(doneButtonItem, animated: false) } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override public func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() for directory in rootSoundDirectories { directories.append(directory) @@ -45,38 +45,38 @@ public class AARingtonesViewController: AATableViewController { } getDirectories() - loadSoundFiles() + // loadSoundFiles() tableView.rowHeight = 44.0 - tableView.sectionIndexBackgroundColor = UIColor.clearColor() + tableView.sectionIndexBackgroundColor = UIColor.clear } - override public func viewWillDisappear(animated: Bool) { + override open func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(true) - if(audioPlayer != nil && audioPlayer.playing){ + if(audioPlayer != nil && audioPlayer.isPlaying){ audioPlayer.stop() } } - public override func viewDidDisappear(animated: Bool) { + open override func viewDidDisappear(_ animated: Bool) { completion(selectedRingtone) } - override public func didReceiveMemoryWarning() { + override open func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } func getDirectories() { - let fileManager: NSFileManager = NSFileManager() + let fileManager: FileManager = FileManager() for directory in rootSoundDirectories { - let directoryURL: NSURL = NSURL(fileURLWithPath: "\(directory)", isDirectory: true) + let directoryURL: URL = URL(fileURLWithPath: "\(directory)", isDirectory: true) do { - if let URLs: [NSURL] = try fileManager.contentsOfDirectoryAtURL(directoryURL, includingPropertiesForKeys: [NSURLIsDirectoryKey], options: NSDirectoryEnumerationOptions()) { + if let URLs: [URL] = try fileManager.contentsOfDirectory(at: directoryURL, includingPropertiesForKeys: [URLResourceKey.isDirectoryKey], options: FileManager.DirectoryEnumerationOptions()) { var urlIsaDirectory: ObjCBool = ObjCBool(false) for url in URLs { - if fileManager.fileExistsAtPath(url.path!, isDirectory: &urlIsaDirectory) { - if urlIsaDirectory { - let directory: String = "\(url.relativePath!)" + if fileManager.fileExists(atPath: url.path, isDirectory: &urlIsaDirectory) { + if urlIsaDirectory.boolValue { + let directory: String = "\(url.relativePath)" let files: [String] = [] let newSoundFile: (directory: String, files: [String]) = (directory, files) directories.append("\(directory)") @@ -91,79 +91,79 @@ public class AARingtonesViewController: AATableViewController { } } - func loadSoundFiles() { - - for i in 0...directories.count-1 { - let fileManager: NSFileManager = NSFileManager() - let directoryURL: NSURL = NSURL(fileURLWithPath: directories[i], isDirectory: true) - - do { - if let URLs: [NSURL] = try fileManager.contentsOfDirectoryAtURL(directoryURL, includingPropertiesForKeys: [NSURLIsDirectoryKey], options: NSDirectoryEnumerationOptions()) { - var urlIsaDirectory: ObjCBool = ObjCBool(false) - for url in URLs { - if fileManager.fileExistsAtPath(url.path!, isDirectory: &urlIsaDirectory) { - if !urlIsaDirectory { - soundFiles[i].files.append("\(url.lastPathComponent!)") - } - } - } - } - } catch { - debugPrint("\(error)") - } - } - } +// func loadSoundFiles() { + +// for i in 0...directories.count-1 { +// let fileManager: FileManager = FileManager() +// let directoryURL: URL = URL(fileURLWithPath: directories[i], isDirectory: true) +// +// do { +// if let URLs: [URL] = try fileManager.contentsOfDirectory(at: directoryURL, includingPropertiesForKeys: [URLResourceKey.isDirectoryKey], options: FileManager.DirectoryEnumerationOptions()) { +// var urlIsaDirectory: ObjCBool = ObjCBool(false) +// for url in URLs { +// if fileManager.fileExists(atPath: url.path, isDirectory: &urlIsaDirectory) { +// if !urlIsaDirectory { +// soundFiles[i].files.append("\(url.lastPathComponent)") +// } +// } +// } +// } +// } catch { +// debugPrint("\(error)") +// } +// } +// } - override public func numberOfSectionsInTableView(tableView: UITableView) -> Int { - return 1 - } - - override public func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return soundFiles[section].files.count - } - - func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { +// override open func numberOfSections(in tableView: UITableView) -> Int { +// return 1 +// } +// +// override open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { +// return soundFiles[section].files.count +// } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return "Ringtones" } - func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + func tableView(_ tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: IndexPath) -> CGFloat { return 44 } - func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + func tableView(_ tableView: UITableView, heightForRowAtIndexPath indexPath: IndexPath) -> CGFloat { return UITableViewAutomaticDimension } - override public func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { - - let fileName: String = soundFiles[indexPath.section].files[indexPath.row] - let cell: AACommonCell = tableView.dequeueCell(indexPath) - cell.style = .Normal - let name = fileName.componentsSeparatedByString(".m4r") - cell.textLabel?.text = name.first - return cell - } +// override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { +// +// let fileName: String = soundFiles[(indexPath as NSIndexPath).section].files[(indexPath as NSIndexPath).row] +// let cell: AACommonCell = tableView.dequeueCell(indexPath) +// cell.style = .normal +// let name = fileName.components(separatedBy: ".m4r") +// cell.textLabel?.text = name.first +// return cell +// } - func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) { - if let cell = tableView.cellForRowAtIndexPath(indexPath) as? AACommonCell { - cell.style = .Normal + func tableView(_ tableView: UITableView, didDeselectRowAtIndexPath indexPath: IndexPath) { + if let cell = tableView.cellForRow(at: indexPath) as? AACommonCell { + cell.style = .normal } } - func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + func tableView(_ tableView: UITableView, didSelectRowAtIndexPath indexPath: IndexPath) { - let directory: String = soundFiles[indexPath.section].directory - let fileName: String = soundFiles[indexPath.section].files[indexPath.row] - let fileURL: NSURL = NSURL(fileURLWithPath: "\(directory)/\(fileName)") + let directory: String = soundFiles[(indexPath as NSIndexPath).section].directory + let fileName: String = soundFiles[(indexPath as NSIndexPath).section].files[(indexPath as NSIndexPath).row] + let fileURL: URL = URL(fileURLWithPath: "\(directory)/\(fileName)") do { - audioPlayer = try AVAudioPlayer(contentsOfURL: fileURL) + audioPlayer = try AVAudioPlayer(contentsOf: fileURL) audioPlayer.play() } catch { debugPrint("\(error)") selectedRingtone = "" } - let cell = tableView.cellForRowAtIndexPath(indexPath) as! AACommonCell - selectedRingtone = soundFiles[indexPath.section].files[indexPath.row] - cell.style = .Checkmark + let cell = tableView.cellForRow(at: indexPath) as! AACommonCell + selectedRingtone = soundFiles[(indexPath as NSIndexPath).section].files[(indexPath as NSIndexPath).row] + cell.style = .checkmark } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Root/AANoSelectionViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Root/AANoSelectionViewController.swift index 73bd7f4bb3..f67c9149be 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Root/AANoSelectionViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Root/AANoSelectionViewController.swift @@ -4,7 +4,7 @@ import Foundation -public class AANoSelectionViewController: AAViewController { +open class AANoSelectionViewController: AAViewController { public override init() { super.init(nibName: nil, bundle: nil) @@ -15,4 +15,4 @@ public class AANoSelectionViewController: AAViewController { public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Root/AARootSplitViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Root/AARootSplitViewController.swift index b71e8d59e5..202d5271e9 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Root/AARootSplitViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Root/AARootSplitViewController.swift @@ -4,13 +4,13 @@ import Foundation -public class AARootSplitViewController: UISplitViewController { +open class AARootSplitViewController: UISplitViewController { public init() { super.init(nibName: nil, bundle: nil) - preferredDisplayMode = .AllVisible - if (interfaceOrientation == UIInterfaceOrientation.Portrait || interfaceOrientation == UIInterfaceOrientation.PortraitUpsideDown) { + preferredDisplayMode = .allVisible + if (interfaceOrientation == UIInterfaceOrientation.portrait || interfaceOrientation == UIInterfaceOrientation.portraitUpsideDown) { minimumPrimaryColumnWidth = CGFloat(300.0) maximumPrimaryColumnWidth = CGFloat(300.0) } else { @@ -23,10 +23,10 @@ public class AARootSplitViewController: UISplitViewController { fatalError("init(coder:) has not been implemented") } - public override func willRotateToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval) { - super.willRotateToInterfaceOrientation(toInterfaceOrientation, duration: duration) + open override func willRotate(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval) { + super.willRotate(to: toInterfaceOrientation, duration: duration) - if (toInterfaceOrientation == UIInterfaceOrientation.Portrait || toInterfaceOrientation == UIInterfaceOrientation.PortraitUpsideDown) { + if (toInterfaceOrientation == UIInterfaceOrientation.portrait || toInterfaceOrientation == UIInterfaceOrientation.portraitUpsideDown) { minimumPrimaryColumnWidth = CGFloat(300.0) maximumPrimaryColumnWidth = CGFloat(300.0) } else { @@ -35,7 +35,7 @@ public class AARootSplitViewController: UISplitViewController { } } - public override func preferredStatusBarStyle() -> UIStatusBarStyle { + open override var preferredStatusBarStyle : UIStatusBarStyle { return ActorSDK.sharedActor().style.vcStatusBarStyle } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Root/AARootTabViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Root/AARootTabViewController.swift index 3665e74b74..2c194378eb 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Root/AARootTabViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Root/AARootTabViewController.swift @@ -5,14 +5,34 @@ import Foundation import UIKit import MessageUI +fileprivate func < (lhs: T?, rhs: T?) -> Bool { + switch (lhs, rhs) { + case let (l?, r?): + return l < r + case (nil, _?): + return true + default: + return false + } +} -public class AARootTabViewController : UITabBarController, MFMessageComposeViewControllerDelegate, UIAlertViewDelegate { +fileprivate func > (lhs: T?, rhs: T?) -> Bool { + switch (lhs, rhs) { + case let (l?, r?): + return l > r + default: + return rhs < lhs + } +} + + +open class AARootTabViewController : UITabBarController, MFMessageComposeViewControllerDelegate, UIAlertViewDelegate { - private let binder = AABinder() + fileprivate let binder = AABinder() - private var appEmptyContainer = UIView() - private var appIsSyncingPlaceholder = AABigPlaceholderView(topOffset: 44 + 20) - private var appIsEmptyPlaceholder = AABigPlaceholderView(topOffset: 44 + 20) + fileprivate var appEmptyContainer = UIView() + fileprivate var appIsSyncingPlaceholder = AABigPlaceholderView(topOffset: 44 + 20) + fileprivate var appIsEmptyPlaceholder = AABigPlaceholderView(topOffset: 44 + 20) public init() { super.init(nibName: nil, bundle: nil) @@ -25,13 +45,13 @@ public class AARootTabViewController : UITabBarController, MFMessageComposeViewC fatalError("Not implemented") } - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() tabBar.barTintColor = ActorSDK.sharedActor().style.tabBgColor - appEmptyContainer.hidden = true - appIsEmptyPlaceholder.hidden = true + appEmptyContainer.isHidden = true + appIsEmptyPlaceholder.isHidden = true appIsEmptyPlaceholder.setImage( UIImage.bundled("contacts_list_placeholder"), title: AALocalized("Placeholder_Empty_Title"), @@ -43,7 +63,7 @@ public class AARootTabViewController : UITabBarController, MFMessageComposeViewC action2Selector: #selector(AARootTabViewController.doAddContact)) appEmptyContainer.addSubview(appIsEmptyPlaceholder) - appIsSyncingPlaceholder.hidden = true + appIsSyncingPlaceholder.isHidden = true appIsSyncingPlaceholder.setImage( UIImage.bundled("chat_list_placeholder"), title: AALocalized("Placeholder_Loading_Title"), @@ -65,23 +85,23 @@ public class AARootTabViewController : UITabBarController, MFMessageComposeViewC } } - public func showAppIsSyncingPlaceholder() { - appIsEmptyPlaceholder.hidden = true - appIsSyncingPlaceholder.hidden = false - appEmptyContainer.hidden = false + open func showAppIsSyncingPlaceholder() { + appIsEmptyPlaceholder.isHidden = true + appIsSyncingPlaceholder.isHidden = false + appEmptyContainer.isHidden = false } - public func showAppIsEmptyPlaceholder() { - appIsEmptyPlaceholder.hidden = false - appIsSyncingPlaceholder.hidden = true - appEmptyContainer.hidden = false + open func showAppIsEmptyPlaceholder() { + appIsEmptyPlaceholder.isHidden = false + appIsSyncingPlaceholder.isHidden = true + appEmptyContainer.isHidden = false } - public func hidePlaceholders() { - appEmptyContainer.hidden = true + open func hidePlaceholders() { + appEmptyContainer.isHidden = true } - public func showSmsInvitation(phone: String?) { + open func showSmsInvitation(_ phone: String?) { if MFMessageComposeViewController.canSendText() { let messageComposeController = MFMessageComposeViewController() messageComposeController.messageComposeDelegate = self @@ -90,20 +110,21 @@ public class AARootTabViewController : UITabBarController, MFMessageComposeViewC } messageComposeController.body = AALocalized("InviteText") .replace("{link}", dest: ActorSDK.sharedActor().inviteUrl) + .replace("{appname}", dest: ActorSDK.sharedActor().appName) messageComposeController.navigationBar.tintColor = ActorSDK.sharedActor().style.navigationTitleColor - presentViewController(messageComposeController, animated: true, completion: { () -> Void in -// ActorSDK.sharedActor().style.appl + present(messageComposeController, animated: true, completion: { () -> Void in + }) } else { UIAlertView(title: "Error", message: "Cannot send SMS", delegate: nil, cancelButtonTitle: "OK").show() } } - public func showSmsInvitation() { + open func showSmsInvitation() { showSmsInvitation(nil) } - public func doAddContact() { + open func doAddContact() { let alertView = UIAlertView( title: AALocalized("ContactsAddHeader"), message: AALocalized("ContactsAddHint"), @@ -111,14 +132,14 @@ public class AARootTabViewController : UITabBarController, MFMessageComposeViewC cancelButtonTitle: AALocalized("AlertCancel"), otherButtonTitles: AALocalized("AlertNext")) - alertView.alertViewStyle = UIAlertViewStyle.PlainTextInput + alertView.alertViewStyle = UIAlertViewStyle.plainTextInput alertView.show() } // MARK: - // MARK: Layout - public override func viewWillLayoutSubviews() { + open override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() appEmptyContainer.frame = view.bounds @@ -126,41 +147,41 @@ public class AARootTabViewController : UITabBarController, MFMessageComposeViewC appIsEmptyPlaceholder.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height) } - public override func shouldAutorotate() -> Bool { + open override var shouldAutorotate : Bool { return false } - public override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask { - return UIInterfaceOrientationMask.Portrait + open override var supportedInterfaceOrientations : UIInterfaceOrientationMask { + return UIInterfaceOrientationMask.portrait } - public override func preferredStatusBarStyle() -> UIStatusBarStyle { + open override var preferredStatusBarStyle : UIStatusBarStyle { return ActorSDK.sharedActor().style.vcStatusBarStyle } - public func messageComposeViewController(controller: MFMessageComposeViewController, didFinishWithResult result: MessageComposeResult) { - controller.dismissViewControllerAnimated(true, completion: nil) + open func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) { + controller.dismiss(animated: true, completion: nil) } - public func alertView(alertView: UIAlertView, clickedButtonAtIndex buttonIndex: Int) { + open func alertView(_ alertView: UIAlertView, clickedButtonAt buttonIndex: Int) { // TODO: Localize if buttonIndex == 1 { - let textField = alertView.textFieldAtIndex(0)! + let textField = alertView.textField(at: 0)! if textField.text?.length > 0 { - self.execute(Actor.findUsersCommandWithQuery(textField.text), successBlock: { (val) -> Void in + self.execute(Actor.findUsersCommand(withQuery: textField.text), successBlock: { (val) -> Void in var user: ACUserVM? user = val as? ACUserVM if user == nil { if let users = val as? IOSObjectArray { if Int(users.length()) > 0 { - if let tempUser = users.objectAtIndex(0) as? ACUserVM { + if let tempUser = users.object(at: 0) as? ACUserVM { user = tempUser } } } } if user != nil { - self.execute(Actor.addContactCommandWithUid(user!.getId())!, successBlock: { (val) -> () in + self.execute(Actor.addContactCommand(withUid: user!.getId())!, successBlock: { (val) -> () in // DO Nothing }, failureBlock: { (val) -> () in self.showSmsInvitation(textField.text) @@ -174,4 +195,4 @@ public class AARootTabViewController : UITabBarController, MFMessageComposeViewC } } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsLastSeenController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsLastSeenController.swift index 2dbd16a6b6..5f84db7daa 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsLastSeenController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsLastSeenController.swift @@ -4,21 +4,21 @@ import UIKit -public class AASettingsLastSeenController: AATableViewController { +open class AASettingsLastSeenController: AATableViewController { - private var privacy = Actor.getPrivacy() + fileprivate var privacy = Actor.getPrivacy() // MARK: - // MARK: Constructors - private let CellIdentifier = "CellIdentifier" + fileprivate let CellIdentifier = "CellIdentifier" public init() { - super.init(style: UITableViewStyle.Grouped) + super.init(style: UITableViewStyle.grouped) title = AALocalized("PrivacyLastSeen") - content = ACAllEvents_Settings.NOTIFICATIONS() + content = ACAllEvents_Settings.notifications() } public required init(coder aDecoder: NSCoder) { @@ -27,10 +27,10 @@ public class AASettingsLastSeenController: AATableViewController { // MARK: - - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() - tableView.registerClass(AACommonCell.self, forCellReuseIdentifier: CellIdentifier) + tableView.register(AACommonCell.self, forCellReuseIdentifier: CellIdentifier) tableView.backgroundColor = appStyle.vcBackyardColor tableView.separatorColor = appStyle.vcSeparatorColor @@ -40,58 +40,58 @@ public class AASettingsLastSeenController: AATableViewController { // MARK: - // MARK: UITableView Data Source - public override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + open override func numberOfSections(in tableView: UITableView) -> Int { return 1 } - public override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 3 } - public func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return nil } - public func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? { + open func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { return nil } - private func lastSeenCell(indexPath: NSIndexPath) -> AACommonCell { - let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) as! AACommonCell + fileprivate func lastSeenCell(_ indexPath: IndexPath) -> AACommonCell { + let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier, for: indexPath) as! AACommonCell - if indexPath.row == 0 { + if (indexPath as NSIndexPath).row == 0 { cell.setContent(AALocalized("PrivacyLastSeenEverybody")) if (self.privacy == "always") { - cell.style = .Checkmark + cell.style = .checkmark } else { - cell.style = .Normal + cell.style = .normal } - } else if indexPath.row == 1 { + } else if (indexPath as NSIndexPath).row == 1 { cell.setContent(AALocalized("PrivacyLastSeenContacts")) if (self.privacy == "contacts") { - cell.style = .Checkmark + cell.style = .checkmark } else { - cell.style = .Normal + cell.style = .normal } - } else if indexPath.row == 2 { + } else if (indexPath as NSIndexPath).row == 2 { cell.setContent(AALocalized("PrivacyLastSeenNone")) if (self.privacy == "none") { - cell.style = .Checkmark + cell.style = .checkmark } else { - cell.style = .Normal + cell.style = .normal } } - cell.selectionStyle = UITableViewCellSelectionStyle.None + cell.selectionStyle = UITableViewCellSelectionStyle.none cell.bottomSeparatorVisible = false cell.topSeparatorVisible = false cell.bottomSeparatorLeftInset = 0 @@ -100,32 +100,32 @@ public class AASettingsLastSeenController: AATableViewController { return cell } - public override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return lastSeenCell(indexPath) } - public func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + open func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { let header: UITableViewHeaderFooterView = view as! UITableViewHeaderFooterView header.textLabel!.textColor = ActorSDK.sharedActor().style.cellFooterColor } - public func tableView(tableView: UITableView, willDisplayFooterView view: UIView, forSection section: Int) { + open func tableView(_ tableView: UITableView, willDisplayFooterView view: UIView, forSection section: Int) { let header: UITableViewHeaderFooterView = view as! UITableViewHeaderFooterView header.textLabel!.textColor = ActorSDK.sharedActor().style.cellFooterColor } - public func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + open func tableView(_ tableView: UITableView, didSelectRowAtIndexPath indexPath: IndexPath) { - if indexPath.row == 0 { + if (indexPath as NSIndexPath).row == 0 { Actor.setPrivacyWithPrivacy("always") - } else if indexPath.row == 1 { + } else if (indexPath as NSIndexPath).row == 1 { Actor.setPrivacyWithPrivacy("contacts") - } else if indexPath.row == 2 { + } else if (indexPath as NSIndexPath).row == 2 { Actor.setPrivacyWithPrivacy("none") diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsMediaViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsMediaViewController.swift index 3498ceb88f..753b51e8d4 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsMediaViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsMediaViewController.swift @@ -4,12 +4,12 @@ import UIKit -public class AASettingsMediaViewController: AAContentTableController { +open class AASettingsMediaViewController: AAContentTableController { - private var sessionsCell: AAManagedArrayRows? + fileprivate var sessionsCell: AAManagedArrayRows? public init() { - super.init(style: AAContentTableStyle.SettingsGrouped) + super.init(style: AAContentTableStyle.settingsGrouped) navigationItem.title = AALocalized("MediaTitle") } @@ -18,14 +18,14 @@ public class AASettingsMediaViewController: AAContentTableController { fatalError("init(coder:) has not been implemented") } - public override func tableDidLoad() { + open override func tableDidLoad() { section { (s) -> () in s.headerText = AALocalized("MediaPhotoDownloadHeader") s.common { (r) -> () in - r.style = .Switch + r.style = .switch r.content = AALocalized("SettingsPrivateChats") r.switchOn = ActorSDK.sharedActor().isPhotoAutoDownloadPrivate @@ -36,7 +36,7 @@ public class AASettingsMediaViewController: AAContentTableController { } s.common { (r) -> () in - r.style = .Switch + r.style = .switch r.content = AALocalized("SettingsGroupChats") r.switchOn = ActorSDK.sharedActor().isPhotoAutoDownloadGroup @@ -52,7 +52,7 @@ public class AASettingsMediaViewController: AAContentTableController { s.headerText = AALocalized("MediaAudioDownloadHeader") s.common { (r) -> () in - r.style = .Switch + r.style = .switch r.content = AALocalized("SettingsPrivateChats") r.switchOn = ActorSDK.sharedActor().isAudioAutoDownloadPrivate @@ -63,7 +63,7 @@ public class AASettingsMediaViewController: AAContentTableController { } s.common { (r) -> () in - r.style = .Switch + r.style = .switch r.content = AALocalized("SettingsGroupChats") r.switchOn = ActorSDK.sharedActor().isAudioAutoDownloadGroup @@ -79,7 +79,7 @@ public class AASettingsMediaViewController: AAContentTableController { s.headerText = AALocalized("MediaOtherHeader") s.common { (r) -> () in - r.style = .Switch + r.style = .switch r.content = AALocalized("MediaAutoplayGif") r.switchOn = ActorSDK.sharedActor().isGIFAutoplayEnabled diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsNotificationsViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsNotificationsViewController.swift index 7a1f6b8ed5..2646e7dd99 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsNotificationsViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsNotificationsViewController.swift @@ -4,19 +4,19 @@ import UIKit -public class AASettingsNotificationsViewController: AATableViewController { +open class AASettingsNotificationsViewController: AATableViewController { // MARK: - // MARK: Constructors - private let CellIdentifier = "CellIdentifier" + fileprivate let CellIdentifier = "CellIdentifier" public init() { - super.init(style: UITableViewStyle.Grouped) + super.init(style: UITableViewStyle.grouped) title = AALocalized("NotificationsTitle") - content = ACAllEvents_Settings.NOTIFICATIONS() + content = ACAllEvents_Settings.notifications() } public required init(coder aDecoder: NSCoder) { @@ -25,12 +25,12 @@ public class AASettingsNotificationsViewController: AATableViewController { // MARK: - - public override func viewDidLoad() { + open override func viewDidLoad() { super.viewDidLoad() - tableView.registerClass(AACommonCell.self, forCellReuseIdentifier: CellIdentifier) + tableView.register(AACommonCell.self, forCellReuseIdentifier: CellIdentifier) tableView.backgroundColor = appStyle.vcBackyardColor - tableView.separatorStyle = UITableViewCellSeparatorStyle.None + tableView.separatorStyle = UITableViewCellSeparatorStyle.none view.backgroundColor = tableView.backgroundColor } @@ -38,11 +38,11 @@ public class AASettingsNotificationsViewController: AATableViewController { // MARK: - // MARK: UITableView Data Source - public override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + open override func numberOfSections(in tableView: UITableView) -> Int { return 4 } - public override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if (section == 0) { return 1 } else if (section == 1) { @@ -56,7 +56,7 @@ public class AASettingsNotificationsViewController: AATableViewController { return 1 } - public func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { if (section == 0) { return AALocalized("NotificationsEffectsTitle") } else if (section == 1) { @@ -70,7 +70,7 @@ public class AASettingsNotificationsViewController: AATableViewController { return nil } - public func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? { + open func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { if (section == 1) { return AALocalized("NotificationsNotificationHint") } else if (section == 2) { @@ -82,12 +82,12 @@ public class AASettingsNotificationsViewController: AATableViewController { return nil } - private func notificationsTonesCell(indexPath: NSIndexPath) -> AACommonCell { - let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) as! AACommonCell + fileprivate func notificationsTonesCell(_ indexPath: IndexPath) -> AACommonCell { + let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier, for: indexPath) as! AACommonCell cell.setContent(AALocalized("NotificationsSoundEffects")) - cell.style = .Switch - cell.selectionStyle = UITableViewCellSelectionStyle.None + cell.style = .switch + cell.selectionStyle = UITableViewCellSelectionStyle.none cell.bottomSeparatorVisible = true cell.topSeparatorVisible = true cell.bottomSeparatorLeftInset = 0 @@ -95,18 +95,18 @@ public class AASettingsNotificationsViewController: AATableViewController { cell.setSwitcherOn(Actor.isConversationTonesEnabled()) cell.switchBlock = { (nValue: Bool) in - Actor.changeConversationTonesEnabledWithValue(nValue) + Actor.changeConversationTonesEnabled(withValue: nValue) } return cell } - private func notificationsEnableCell(indexPath: NSIndexPath) -> AACommonCell { - let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) as! AACommonCell + fileprivate func notificationsEnableCell(_ indexPath: IndexPath) -> AACommonCell { + let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier, for: indexPath) as! AACommonCell cell.setContent(AALocalized("NotificationsEnable")) - cell.style = .Switch - cell.selectionStyle = UITableViewCellSelectionStyle.None + cell.style = .switch + cell.selectionStyle = UITableViewCellSelectionStyle.none cell.bottomSeparatorVisible = true cell.topSeparatorVisible = true cell.bottomSeparatorLeftInset = 0 @@ -115,12 +115,12 @@ public class AASettingsNotificationsViewController: AATableViewController { cell.setSwitcherOn(Actor.isNotificationsEnabled()) cell.switchBlock = { (nValue: Bool) in self.tableView.beginUpdates() - Actor.changeNotificationsEnabledWithValue(nValue) - let rows = [NSIndexPath(forRow: 1, inSection: indexPath.section)] + Actor.changeNotificationsEnabled(withValue: nValue) + let rows = [IndexPath(row: 1, section: (indexPath as NSIndexPath).section)] if (nValue) { - self.tableView.insertRowsAtIndexPaths(rows, withRowAnimation: UITableViewRowAnimation.Middle) + self.tableView.insertRows(at: rows, with: UITableViewRowAnimation.middle) } else { - self.tableView.deleteRowsAtIndexPaths(rows, withRowAnimation: UITableViewRowAnimation.Middle) + self.tableView.deleteRows(at: rows, with: UITableViewRowAnimation.middle) } self.tableView.endUpdates() } @@ -128,12 +128,12 @@ public class AASettingsNotificationsViewController: AATableViewController { return cell } - private func notificationsAlertCell(indexPath: NSIndexPath) -> AACommonCell { - let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) as! AACommonCell + fileprivate func notificationsAlertCell(_ indexPath: IndexPath) -> AACommonCell { + let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier, for: indexPath) as! AACommonCell cell.setContent(AALocalized("NotificationsSound")) - cell.style = .Switch - cell.selectionStyle = UITableViewCellSelectionStyle.None + cell.style = .switch + cell.selectionStyle = UITableViewCellSelectionStyle.none cell.bottomSeparatorVisible = true //cell.topSeparatorVisible = true cell.bottomSeparatorLeftInset = 0 @@ -142,19 +142,19 @@ public class AASettingsNotificationsViewController: AATableViewController { cell.setSwitcherOn(Actor.isNotificationSoundEnabled()) cell.setSwitcherEnabled(Actor.isNotificationsEnabled()) cell.switchBlock = { (nValue: Bool) in - Actor.changeNotificationSoundEnabledWithValue(nValue) + Actor.changeNotificationSoundEnabled(withValue: nValue) } return cell } - private func groupEnabledCell(indexPath: NSIndexPath) -> AACommonCell { - let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) as! AACommonCell + fileprivate func groupEnabledCell(_ indexPath: IndexPath) -> AACommonCell { + let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier, for: indexPath) as! AACommonCell cell.setContent(AALocalized("NotificationsEnable")) - cell.style = .Switch - cell.selectionStyle = UITableViewCellSelectionStyle.None + cell.style = .switch + cell.selectionStyle = UITableViewCellSelectionStyle.none cell.topSeparatorVisible = true cell.bottomSeparatorVisible = true cell.bottomSeparatorLeftInset = 0 @@ -164,11 +164,11 @@ public class AASettingsNotificationsViewController: AATableViewController { cell.switchBlock = { (nValue: Bool) in self.tableView.beginUpdates() Actor.changeGroupNotificationsEnabled(nValue) - let rows = [NSIndexPath(forRow: 1, inSection: indexPath.section)] + let rows = [IndexPath(row: 1, section: (indexPath as NSIndexPath).section)] if (nValue) { - self.tableView.insertRowsAtIndexPaths(rows, withRowAnimation: UITableViewRowAnimation.Middle) + self.tableView.insertRows(at: rows, with: UITableViewRowAnimation.middle) } else { - self.tableView.deleteRowsAtIndexPaths(rows, withRowAnimation: UITableViewRowAnimation.Middle) + self.tableView.deleteRows(at: rows, with: UITableViewRowAnimation.middle) } self.tableView.endUpdates() } @@ -176,12 +176,12 @@ public class AASettingsNotificationsViewController: AATableViewController { return cell } - private func groupEnabledMentionsCell(indexPath: NSIndexPath) -> AACommonCell { - let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) as! AACommonCell + fileprivate func groupEnabledMentionsCell(_ indexPath: IndexPath) -> AACommonCell { + let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier, for: indexPath) as! AACommonCell cell.setContent(AALocalized("NotificationsOnlyMentions")) - cell.style = .Switch - cell.selectionStyle = UITableViewCellSelectionStyle.None + cell.style = .switch + cell.selectionStyle = UITableViewCellSelectionStyle.none cell.bottomSeparatorVisible = true //cell.topSeparatorVisible = true cell.bottomSeparatorLeftInset = 0 @@ -196,12 +196,12 @@ public class AASettingsNotificationsViewController: AATableViewController { return cell } - private func inAppAlertCell(indexPath: NSIndexPath) -> AACommonCell { - let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) as! AACommonCell + fileprivate func inAppAlertCell(_ indexPath: IndexPath) -> AACommonCell { + let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier, for: indexPath) as! AACommonCell cell.setContent(AALocalized("NotificationsEnable")) - cell.style = .Switch - cell.selectionStyle = UITableViewCellSelectionStyle.None + cell.style = .switch + cell.selectionStyle = UITableViewCellSelectionStyle.none cell.bottomSeparatorVisible = true cell.topSeparatorVisible = true cell.bottomSeparatorLeftInset = 0 @@ -210,12 +210,12 @@ public class AASettingsNotificationsViewController: AATableViewController { cell.setSwitcherOn(Actor.isInAppNotificationsEnabled()) cell.switchBlock = { (nValue: Bool) in self.tableView.beginUpdates() - Actor.changeInAppNotificationsEnabledWithValue(nValue) - let rows = [NSIndexPath(forRow: 1, inSection: 3), NSIndexPath(forRow: 2, inSection: 3)] + Actor.changeInAppNotificationsEnabled(withValue: nValue) + let rows = [IndexPath(row: 1, section: 3), IndexPath(row: 2, section: 3)] if (nValue) { - self.tableView.insertRowsAtIndexPaths(rows, withRowAnimation: UITableViewRowAnimation.Middle) + self.tableView.insertRows(at: rows, with: UITableViewRowAnimation.middle) } else { - self.tableView.deleteRowsAtIndexPaths(rows, withRowAnimation: UITableViewRowAnimation.Middle) + self.tableView.deleteRows(at: rows, with: UITableViewRowAnimation.middle) } self.tableView.endUpdates() } @@ -223,12 +223,12 @@ public class AASettingsNotificationsViewController: AATableViewController { return cell } - private func inAppSoundCell(indexPath: NSIndexPath) -> AACommonCell { - let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) as! AACommonCell + fileprivate func inAppSoundCell(_ indexPath: IndexPath) -> AACommonCell { + let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier, for: indexPath) as! AACommonCell cell.setContent(AALocalized("NotificationsSound")) - cell.style = .Switch - cell.selectionStyle = UITableViewCellSelectionStyle.None + cell.style = .switch + cell.selectionStyle = UITableViewCellSelectionStyle.none cell.bottomSeparatorVisible = true // cell.topSeparatorVisible = true cell.bottomSeparatorLeftInset = 0 @@ -238,19 +238,19 @@ public class AASettingsNotificationsViewController: AATableViewController { cell.setSwitcherEnabled(Actor.isInAppNotificationsEnabled()) cell.switchBlock = { (nValue: Bool) in - Actor.changeInAppNotificationSoundEnabledWithValue(nValue) + Actor.changeInAppNotificationSoundEnabled(withValue: nValue) } return cell } - private func inAppVibrateCell(indexPath: NSIndexPath) -> AACommonCell { - let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) as! AACommonCell + fileprivate func inAppVibrateCell(_ indexPath: IndexPath) -> AACommonCell { + let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier, for: indexPath) as! AACommonCell cell.setContent(AALocalized("NotificationsVibration")) - cell.style = .Switch - cell.selectionStyle = UITableViewCellSelectionStyle.None + cell.style = .switch + cell.selectionStyle = UITableViewCellSelectionStyle.none cell.bottomSeparatorVisible = true //cell.topSeparatorVisible = true cell.bottomSeparatorLeftInset = 0 @@ -260,18 +260,18 @@ public class AASettingsNotificationsViewController: AATableViewController { cell.setSwitcherEnabled(Actor.isInAppNotificationsEnabled()) cell.switchBlock = { (nValue: Bool) in - Actor.changeInAppNotificationVibrationEnabledWithValue(nValue) + Actor.changeInAppNotificationVibrationEnabled(withValue: nValue) } return cell } - private func notificationsPreviewCell(indexPath: NSIndexPath) -> AACommonCell { - let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) as! AACommonCell + fileprivate func notificationsPreviewCell(_ indexPath: IndexPath) -> AACommonCell { + let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier, for: indexPath) as! AACommonCell cell.setContent(AALocalized("NotificationsPreview")) - cell.style = .Switch - cell.selectionStyle = UITableViewCellSelectionStyle.None + cell.style = .switch + cell.selectionStyle = UITableViewCellSelectionStyle.none cell.bottomSeparatorVisible = true cell.topSeparatorVisible = true cell.bottomSeparatorLeftInset = 0 @@ -279,40 +279,40 @@ public class AASettingsNotificationsViewController: AATableViewController { cell.setSwitcherOn(Actor.isShowNotificationsText()) cell.switchBlock = { (nValue: Bool) in - Actor.changeShowNotificationTextEnabledWithValue(nValue) + Actor.changeShowNotificationTextEnabled(withValue: nValue) } return cell } - public override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { - if indexPath.section == 0 { + open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if (indexPath as NSIndexPath).section == 0 { return notificationsTonesCell(indexPath) - } else if (indexPath.section == 1) { - if (indexPath.row == 0) { + } else if ((indexPath as NSIndexPath).section == 1) { + if ((indexPath as NSIndexPath).row == 0) { return notificationsEnableCell(indexPath) - } else if (indexPath.row == 1) { + } else if ((indexPath as NSIndexPath).row == 1) { return notificationsAlertCell(indexPath) } - } else if (indexPath.section == 2) { - if (indexPath.row == 0) { + } else if ((indexPath as NSIndexPath).section == 2) { + if ((indexPath as NSIndexPath).row == 0) { return groupEnabledCell(indexPath) } else { return groupEnabledMentionsCell(indexPath) } - } else if (indexPath.section == 3) { + } else if ((indexPath as NSIndexPath).section == 3) { return notificationsPreviewCell(indexPath) } return UITableViewCell() } - public func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + open func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { let header: UITableViewHeaderFooterView = view as! UITableViewHeaderFooterView header.textLabel!.textColor = ActorSDK.sharedActor().style.cellFooterColor } - public func tableView(tableView: UITableView, willDisplayFooterView view: UIView, forSection section: Int) { + open func tableView(_ tableView: UITableView, willDisplayFooterView view: UIView, forSection section: Int) { let header: UITableViewHeaderFooterView = view as! UITableViewHeaderFooterView header.textLabel!.textColor = ActorSDK.sharedActor().style.cellFooterColor } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsPrivacyViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsPrivacyViewController.swift index 016acc956f..bf92ea9aa2 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsPrivacyViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsPrivacyViewController.swift @@ -4,23 +4,23 @@ import UIKit -public class AASettingsPrivacyViewController: AAContentTableController { +open class AASettingsPrivacyViewController: AAContentTableController { - private var sessionsCell: AAManagedArrayRows? + fileprivate var sessionsCell: AAManagedArrayRows? public init() { - super.init(style: AAContentTableStyle.SettingsGrouped) + super.init(style: AAContentTableStyle.settingsGrouped) navigationItem.title = AALocalized("PrivacyTitle") - content = ACAllEvents_Settings.PRIVACY() + content = ACAllEvents_Settings.privacy() } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public override func tableDidLoad() { + open override func tableDidLoad() { section { (s) -> () in diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsSessionsController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsSessionsController.swift index 965c1019c1..eff004cdf0 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsSessionsController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsSessionsController.swift @@ -4,23 +4,23 @@ import UIKit -public class AASettingsSessionsController: AAContentTableController { +open class AASettingsSessionsController: AAContentTableController { - private var sessionsCell: AAManagedArrayRows? + fileprivate var sessionsCell: AAManagedArrayRows? public init() { - super.init(style: AAContentTableStyle.SettingsGrouped) + super.init(style: AAContentTableStyle.settingsGrouped) navigationItem.title = AALocalized("PrivacyAllSessions") - content = ACAllEvents_Settings.PRIVACY() + content = ACAllEvents_Settings.privacy() } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public override func tableDidLoad() { + open override func tableDidLoad() { section { (s) -> () in @@ -42,20 +42,20 @@ public class AASettingsSessionsController: AAContentTableController { section { (s) -> () in self.sessionsCell = s.arrays() { (r: AAManagedArrayRows) -> () in r.bindData = { (c: AACommonCell, d: ARApiAuthSession) -> () in - if d.getAuthHolder().ordinal() != ARApiAuthHolder.THISDEVICE().ordinal() { - c.style = .Normal + if d.getAuthHolder().ordinal() != ARApiAuthHolder.thisdevice().ordinal() { + c.style = .normal c.setContent(d.getDeviceTitle()) } else { - c.style = .Hint + c.style = .hint c.setContent("(Current) \(d.getDeviceTitle())") } } r.selectAction = { (d) -> Bool in - if d.getAuthHolder().ordinal() != ARApiAuthHolder.THISDEVICE().ordinal() { + if d.getAuthHolder().ordinal() != ARApiAuthHolder.thisdevice().ordinal() { self.confirmDangerSheetUser("PrivacyTerminateAlertSingle", tapYes: { [unowned self] () -> () in // Terminating session and reload list - self.executeSafe(Actor.terminateSessionCommandWithId(d.getId()), successBlock: { [unowned self] (val) -> Void in + self.executeSafe(Actor.terminateSessionCommand(withId: d.getId()), successBlock: { [unowned self] (val) -> Void in self.loadSessions() }) }, tapNo: nil) @@ -70,7 +70,7 @@ public class AASettingsSessionsController: AAContentTableController { loadSessions() } - private func loadSessions() { + fileprivate func loadSessions() { execute(Actor.loadSessionsCommand(), successBlock: { [unowned self] (val) -> Void in self.sessionsCell!.data = (val as! JavaUtilList).toArray().toSwiftArray() self.managedTable.tableView.reloadData() diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsViewController.swift index 1d7b8bb237..e853061b1b 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsViewController.swift @@ -5,21 +5,21 @@ import UIKit import MobileCoreServices -public class AASettingsViewController: AAContentTableController { +open class AASettingsViewController: AAContentTableController { - private var phonesCells: AAManagedArrayRows! - private var emailCells: AAManagedArrayRows! + fileprivate var phonesCells: AAManagedArrayRows! + fileprivate var emailCells: AAManagedArrayRows! - private var headerCell: AAAvatarRow! - private var nicknameCell: AATitledRow! - private var aboutCell: AATitledRow! + fileprivate var headerCell: AAAvatarRow! + fileprivate var nicknameCell: AATitledRow! + fileprivate var aboutCell: AATitledRow! public init() { - super.init(style: AAContentTableStyle.SettingsPlain) + super.init(style: AAContentTableStyle.settingsPlain) uid = Int(Actor.myUid()) - content = ACAllEvents_Main.SETTINGS() + content = ACAllEvents_Main.settings() tabBarItem = UITabBarItem(title: "TabSettings", img: "TabIconSettings", selImage: "TabIconSettingsHighlighted") @@ -30,7 +30,7 @@ public class AASettingsViewController: AAContentTableController { fatalError("init(coder:) has not been implemented") } - public override func tableDidLoad() { + open override func tableDidLoad() { // Profile section { [unowned self] (s) -> () in @@ -43,13 +43,13 @@ public class AASettingsViewController: AAContentTableController { let upload = Actor.getOwnAvatarVM()!.uploadState.get() as? ACAvatarUploadState let avatar = self.user.getAvatarModel().get() let presence = self.user.getPresenceModel().get() - let presenceText = Actor.getFormatter().formatPresence(presence, withSex: self.user.getSex()) + let presenceText = Actor.getFormatter().formatPresence(presence, with: self.user.getSex()) let name = self.user.getNameModel().get() r.id = self.uid r.title = name - if (upload != nil && upload!.isUploading.boolValue) { + if (upload != nil && upload!.isUploading) { r.avatar = nil r.avatarPath = upload!.descriptor r.avatarLoading = true @@ -61,7 +61,7 @@ public class AASettingsViewController: AAContentTableController { if presenceText != nil { r.subtitle = presenceText - if presence!.state.ordinal() == ACUserPresence_State.ONLINE().ordinal() { + if presence!.state.ordinal() == ACUserPresence_State.online().ordinal() { r.subtitleColor = ActorSDK.sharedActor().style.userOnlineColor } else { r.subtitleColor = ActorSDK.sharedActor().style.userOfflineColor @@ -73,13 +73,13 @@ public class AASettingsViewController: AAContentTableController { r.avatarDidTap = { [unowned self] (view: UIView) -> () in let avatar = self.user.getAvatarModel().get() - if avatar != nil && avatar.fullImage != nil { + if avatar != nil && avatar?.fullImage != nil { - let full = avatar.fullImage.fileReference - let small = avatar.smallImage.fileReference - let size = CGSize(width: Int(avatar.fullImage.width), height: Int(avatar.fullImage.height)) + let full = avatar?.fullImage.fileReference + let small = avatar?.smallImage.fileReference + let size = CGSize(width: Int((avatar?.fullImage.width)!), height: Int((avatar?.fullImage.height)!)) - self.presentViewController(AAPhotoPreviewController(file: full, previewFile: small, size: size, fromView: view), animated: true, completion: nil) + self.present(AAPhotoPreviewController(file: full!, previewFile: small, size: size, fromView: view), animated: true, completion: nil) } } } @@ -87,8 +87,8 @@ public class AASettingsViewController: AAContentTableController { // Profile: Set Photo s.action("SettingsSetPhoto") { [unowned self] (r) -> () in r.selectAction = { [unowned self] () -> Bool in - let hasCamera = UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.Camera) - let view = self.tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 1, inSection: 0))!.contentView + let hasCamera = UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera) + let view = self.tableView.cellForRow(at: IndexPath(row: 1, section: 0))!.contentView self.showActionSheet(hasCamera ? ["PhotoCamera", "PhotoLibrary"] : ["PhotoLibrary"], cancelButton: "AlertCancel", destructButton: self.user.getAvatarModel().get() != nil ? "PhotoRemove" : nil, @@ -122,7 +122,7 @@ public class AASettingsViewController: AAContentTableController { c.initialText = self.user.getNameModel().get() - c.fieldAutocapitalizationType = .Words + c.fieldAutocapitalizationType = .words c.fieldHint = "SettingsEditFieldHint" c.didDoneTap = { (t, c) -> () in @@ -131,8 +131,8 @@ public class AASettingsViewController: AAContentTableController { return } - c.executeSafeOnlySuccess(Actor.editMyNameCommandWithName(t)!) { (val) -> Void in - c.dismiss() + c.executeSafeOnlySuccess(Actor.editMyNameCommand(withName: t)!) { (val) -> Void in + c.dismissController() } } } @@ -165,7 +165,7 @@ public class AASettingsViewController: AAContentTableController { r.height = 230 r.closure = { [unowned self] (cell) -> () in cell.wallpapperDidTap = { [unowned self] (name) -> () in - self.presentViewController(AAWallpapperPreviewController(imageName: name), animated: true, completion: nil) + self.present(AAWallpapperPreviewController(imageName: name), animated: true, completion: nil) } cell.bind() } @@ -186,7 +186,7 @@ public class AASettingsViewController: AAContentTableController { // Contacts: Nicknames self.nicknameCell = s.titled("ProfileUsername") { [unowned self] (r) -> () in - r.accessoryType = .DisclosureIndicator + r.accessoryType = .disclosureIndicator r.bindAction = { [unowned self] (r) -> () in if let nick = self.user.getNickModel().get() { @@ -210,8 +210,8 @@ public class AASettingsViewController: AAContentTableController { } c.fieldHint = "SettingsUsernameHintField" - c.fieldAutocorrectionType = .No - c.fieldAutocapitalizationType = .None + c.fieldAutocorrectionType = .no + c.fieldAutocapitalizationType = .none c.hint = "SettingsUsernameHint" c.didDoneTap = { (t, c) -> () in @@ -219,8 +219,8 @@ public class AASettingsViewController: AAContentTableController { if nNick?.length == 0 { nNick = nil } - c.executeSafeOnlySuccess(Actor.editMyNickCommandWithNick(nNick)!, successBlock: { (val) -> Void in - c.dismiss() + c.executeSafeOnlySuccess(Actor.editMyNickCommand(withNick: nNick)!, successBlock: { (val) -> Void in + c.dismissController() }) } } @@ -232,7 +232,7 @@ public class AASettingsViewController: AAContentTableController { // Contacts: About self.aboutCell = s.titled("ProfileAbout") { [unowned self] (r) -> () in - r.accessoryType = .DisclosureIndicator + r.accessoryType = .disclosureIndicator r.bindAction = { [unowned self] (r) -> () in if let about = self.user.getAboutModel().get() { @@ -260,8 +260,8 @@ public class AASettingsViewController: AAContentTableController { if updatedText?.length == 0 { updatedText = nil } - controller.executeSafeOnlySuccess(Actor.editMyAboutCommandWithNick(updatedText)!, successBlock: { (val) -> Void in - controller.dismiss() + controller.executeSafeOnlySuccess(Actor.editMyAboutCommand(withNick: updatedText)!, successBlock: { (val) -> Void in + controller.dismissController() }) } } @@ -280,7 +280,7 @@ public class AASettingsViewController: AAContentTableController { r.bindData = { (c: AATitledCell, d: ACUserPhone) -> () in c.setContent(AALocalized("SettingsMobilePhone"), content: "+\(d.phone)", isAction: false) - c.accessoryType = .None + c.accessoryType = .none } r.bindCopy = { (d: ACUserPhone) -> String? in @@ -288,9 +288,9 @@ public class AASettingsViewController: AAContentTableController { } r.selectAction = { [unowned self] (d: ACUserPhone) -> Bool in - let hasPhone = UIApplication.sharedApplication().canOpenURL(NSURL(string: "telprompt://")!) + let hasPhone = UIApplication.shared.canOpenURL(URL(string: "telprompt://")!) if (!hasPhone) { - UIPasteboard.generalPasteboard().string = "+\(d.phone)" + UIPasteboard.general.string = "+\(d.phone)" self.alertUser("NumberCopied") } return true @@ -305,7 +305,7 @@ public class AASettingsViewController: AAContentTableController { r.bindData = { (c: AATitledCell, d: ACUserEmail) -> () in c.setContent(d.title, content: d.email, isAction: false) - c.accessoryType = .None + c.accessoryType = .none } r.bindCopy = { (d: ACUserEmail) -> String? in @@ -329,19 +329,19 @@ public class AASettingsViewController: AAContentTableController { if let account = ActorSDK.sharedActor().supportAccount { s.navigate("SettingsAskQuestion", closure: { (r) -> () in r.selectAction = { () -> Bool in - self.executeSafe(Actor.findUsersCommandWithQuery(account)) { (val) -> Void in + self.executeSafe(Actor.findUsersCommand(withQuery: account)) { (val) -> Void in var user:ACUserVM! if let users = val as? IOSObjectArray { if Int(users.length()) > 0 { - if let tempUser = users.objectAtIndex(0) as? ACUserVM { + if let tempUser = users.object(at: 0) as? ACUserVM { user = tempUser } } } - if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer.userWithInt(user.getId())) { + if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer.user(with: user.getId())) { self.navigateDetail(customController) } else { - self.navigateDetail(ConversationViewController(peer: ACPeer.userWithInt(user.getId()))) + self.navigateDetail(ConversationViewController(peer: ACPeer.user(with: user.getId()))) } } return true @@ -360,14 +360,14 @@ public class AASettingsViewController: AAContentTableController { } // Support: App version - let version = NSBundle.mainBundle().infoDictionary!["CFBundleShortVersionString"] as! String + let version = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String s.hint(AALocalized("SettingsVersion").replace("{version}", dest: version)) ActorSDK.sharedActor().delegate.actorSettingsSupportDidCreated(self, section: s) } } - public override func tableWillBind(binder: AABinder) { + open override func tableWillBind(_ binder: AABinder) { // Header diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsWallpapersController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsWallpapersController.swift index 9ec3dea054..4830589d12 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsWallpapersController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsWallpapersController.swift @@ -11,11 +11,11 @@ class AASettingsWallpapersController: AATableViewController { // MARK: - // MARK: Constructors - private let CellIdentifier = "CellIdentifier" + fileprivate let CellIdentifier = "CellIdentifier" init() { - super.init(style: UITableViewStyle.Grouped) + super.init(style: UITableViewStyle.grouped) title = AALocalized("WallpapersTitle") } @@ -27,7 +27,7 @@ class AASettingsWallpapersController: AATableViewController { override func viewDidLoad() { super.viewDidLoad() - tableView.registerClass(AAWallpapersCell.self, forCellReuseIdentifier: CellIdentifier) + tableView.register(AAWallpapersCell.self, forCellReuseIdentifier: CellIdentifier) tableView.backgroundColor = appStyle.vcBackyardColor tableView.separatorColor = appStyle.vcSeparatorColor @@ -37,45 +37,45 @@ class AASettingsWallpapersController: AATableViewController { // MARK: - // MARK: UITableView Data Source - override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + override func numberOfSections(in tableView: UITableView) -> Int { return 2 } - override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 } - override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { - if indexPath.section == 0 { + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if (indexPath as NSIndexPath).section == 0 { return photosLibrary(indexPath) } else { return wallpapersCell(indexPath) } } - func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + func tableView(_ tableView: UITableView, didSelectRowAtIndexPath indexPath: IndexPath) { // - self.tableView.deselectRowAtIndexPath(indexPath, animated: true) + self.tableView.deselectRow(at: indexPath, animated: true) - if indexPath.section == 0 { + if (indexPath as NSIndexPath).section == 0 { - self.pickImage(.PhotoLibrary) + self.pickImage(.photoLibrary) } } - func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return nil } - func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? { + func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { return nil } - func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { - if indexPath.section == 0 { + func tableView(_ tableView: UITableView, heightForRowAtIndexPath indexPath: IndexPath) -> CGFloat { + if (indexPath as NSIndexPath).section == 0 { return 40 } else { return 180 @@ -85,23 +85,23 @@ class AASettingsWallpapersController: AATableViewController { // MARK: - // MARK: Create cells - private func photosLibrary(indexPath: NSIndexPath) -> AACommonCell { + fileprivate func photosLibrary(_ indexPath: IndexPath) -> AACommonCell { let cell = AACommonCell() cell.textLabel?.text = AALocalized("WallpapersPhoto") - cell.style = .Navigation + cell.style = .navigation cell.textLabel?.textColor = appStyle.cellTextColor return cell } - private func wallpapersCell(indexPath: NSIndexPath) -> AAWallpapersCell { - let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath) as! AAWallpapersCell + fileprivate func wallpapersCell(_ indexPath: IndexPath) -> AAWallpapersCell { + let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier, for: indexPath) as! AAWallpapersCell - cell.selectionStyle = UITableViewCellSelectionStyle.None + cell.selectionStyle = UITableViewCellSelectionStyle.none cell.wallpapperDidTap = { [unowned self] (name) -> () in - self.presentViewController(AAWallpapperPreviewController(imageName: name), animated: true, completion: nil) + self.present(AAWallpapperPreviewController(imageName: name), animated: true, completion: nil) } return cell @@ -110,13 +110,13 @@ class AASettingsWallpapersController: AATableViewController { // MARK: - // MARK: Picker delegate - override func imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) { + override func imagePickerController(_ picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) { picker.navigationController?.pushViewController(AAWallpapperPreviewController(selectedImage: image), animated: true) } - override func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) { + override func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { if let image = info[UIImagePickerControllerOriginalImage] as? UIImage { picker.pushViewController(AAWallpapperPreviewController(selectedImage: image), animated: true) @@ -124,20 +124,20 @@ class AASettingsWallpapersController: AATableViewController { } - override func imagePickerControllerDidCancel(picker: UIImagePickerController) { - picker.dismissViewControllerAnimated(true, completion: nil) + override func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true, completion: nil) } // MARK: - // MARK: Image picking - func pickImage(source: UIImagePickerControllerSourceType) { + func pickImage(_ source: UIImagePickerControllerSourceType) { let pickerController = AAImagePickerController() pickerController.sourceType = source pickerController.mediaTypes = [kUTTypeImage as String] pickerController.delegate = self - self.presentViewController(pickerController, animated: true, completion: nil) + self.present(pickerController, animated: true, completion: nil) } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsWallpapper.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsWallpapper.swift index ad9613b6dc..88094655ce 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsWallpapper.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AASettingsWallpapper.swift @@ -4,7 +4,7 @@ import Foundation -public class AASettingsWallpapper: AACollectionViewController, UICollectionViewDelegateFlowLayout { +open class AASettingsWallpapper: AACollectionViewController, UICollectionViewDelegateFlowLayout { let padding: CGFloat = 8 @@ -13,7 +13,7 @@ public class AASettingsWallpapper: AACollectionViewController, UICollectionViewD navigationItem.title = AALocalized("WallpapersTitle") - collectionView.registerClass(AAWallpapperPreviewCell.self, forCellWithReuseIdentifier: "cell") + collectionView.register(AAWallpapperPreviewCell.self, forCellWithReuseIdentifier: "cell") collectionView.contentInset = UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding) collectionView.backgroundColor = ActorSDK.sharedActor().style.vcBgColor view.backgroundColor = ActorSDK.sharedActor().style.vcBgColor @@ -23,29 +23,29 @@ public class AASettingsWallpapper: AACollectionViewController, UICollectionViewD fatalError("init(coder:) has not been implemented") } - override public func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + override open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 100 } - override public func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { - let res = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as! AAWallpapperPreviewCell - res.bind(indexPath.item % 3) + override open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let res = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! AAWallpapperPreviewCell + res.bind((indexPath as NSIndexPath).item % 3) return res } - public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { + open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let w = (collectionView.width - 4 * padding) / 3 - let h = w * (UIScreen.mainScreen().bounds.height / UIScreen.mainScreen().bounds.width) + let h = w * (UIScreen.main.bounds.height / UIScreen.main.bounds.width) return CGSize(width: w, height: h) } - public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat { + open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { return padding } - public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat { + open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { return padding } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AAWallpapersCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AAWallpapersCell.swift index d2dec5bfe4..a7f1fae686 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AAWallpapersCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/AAWallpapersCell.swift @@ -4,44 +4,44 @@ import UIKit -public class AAWallpapersCell: AATableViewCell { +open class AAWallpapersCell: AATableViewCell { - private let wallpapper1 = UIImageView() - private let wallpapper2 = UIImageView() - private let wallpapper3 = UIImageView() + fileprivate let wallpapper1 = UIImageView() + fileprivate let wallpapper2 = UIImageView() + fileprivate let wallpapper3 = UIImageView() - public var wallpapperDidTap: ((name: String) -> ())? + open var wallpapperDidTap: ((_ name: String) -> ())? public override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) wallpapper1.clipsToBounds = true - wallpapper1.contentMode = .ScaleAspectFill + wallpapper1.contentMode = .scaleAspectFill wallpapper1.image = UIImage.bundled("bg_1_preview.jpg")! - wallpapper1.viewDidTap = { [unowned self] () -> () in self.wallpapperDidTap?(name: "bg_1.jpg") } + wallpapper1.viewDidTap = { [unowned self] () -> () in self.wallpapperDidTap?("bg_1.jpg") } wallpapper2.clipsToBounds = true - wallpapper2.contentMode = .ScaleAspectFill + wallpapper2.contentMode = .scaleAspectFill wallpapper2.image = UIImage.bundled("bg_2_preview.jpg")! - wallpapper2.viewDidTap = { [unowned self] () -> () in self.wallpapperDidTap?(name: "bg_2.jpg") } + wallpapper2.viewDidTap = { [unowned self] () -> () in self.wallpapperDidTap?("bg_2.jpg") } wallpapper3.clipsToBounds = true - wallpapper3.contentMode = .ScaleAspectFill + wallpapper3.contentMode = .scaleAspectFill wallpapper3.image = UIImage.bundled("bg_3_preview.jpg")! - wallpapper3.viewDidTap = { [unowned self] () -> () in self.wallpapperDidTap?(name: "bg_3.jpg") } + wallpapper3.viewDidTap = { [unowned self] () -> () in self.wallpapperDidTap?("bg_3.jpg") } self.contentView.addSubview(wallpapper1) self.contentView.addSubview(wallpapper2) self.contentView.addSubview(wallpapper3) - selectionStyle = .None + selectionStyle = .none } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() let width = contentView.width @@ -51,9 +51,9 @@ public class AAWallpapersCell: AATableViewCell { let wWidth = (width - padding * 4) / 3 let wHeight = height - padding - 15 - wallpapper1.frame = CGRectMake(padding, wPadding, wWidth, wHeight) - wallpapper2.frame = CGRectMake(padding * 2 + wWidth, wPadding, wWidth, wHeight) - wallpapper3.frame = CGRectMake(padding * 3 + wWidth * 2, wPadding, wWidth, wHeight) + wallpapper1.frame = CGRect(x: padding, y: wPadding, width: wWidth, height: wHeight) + wallpapper2.frame = CGRect(x: padding * 2 + wWidth, y: wPadding, width: wWidth, height: wHeight) + wallpapper3.frame = CGRect(x: padding * 3 + wWidth * 2, y: wPadding, width: wWidth, height: wHeight) } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/Cells/AAWallpapperPreviewCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/Cells/AAWallpapperPreviewCell.swift index d8212e0cd5..bd4c3003f3 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/Cells/AAWallpapperPreviewCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/Cells/AAWallpapperPreviewCell.swift @@ -6,13 +6,13 @@ import Foundation class AAWallpapperPreviewCell: UICollectionViewCell { - private let imageView = UIImageView() - private let imageIcon = UIImageView() + fileprivate let imageView = UIImageView() + fileprivate let imageIcon = UIImageView() override init(frame: CGRect) { super.init(frame: frame) - imageView.contentMode = .ScaleAspectFill + imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true imageIcon.image = UIImage.bundled("ImageSelectedOn") @@ -34,15 +34,15 @@ class AAWallpapperPreviewCell: UICollectionViewCell { fatalError("init(coder:) has not been implemented") } - func bind(index: Int) { + func bind(_ index: Int) { imageView.image = UIImage.bundled("bg_\(index + 1).jpg") - imageIcon.hidden = ActorSDK.sharedActor().messenger.getSelectedWallpaper() == "local:bg_\(index + 1).jpg" + imageIcon.isHidden = ActorSDK.sharedActor().messenger.getSelectedWallpaper() == "local:bg_\(index + 1).jpg" } override func layoutSubviews() { super.layoutSubviews() imageView.frame = contentView.bounds - imageIcon.frame = CGRectMake(contentView.width - 32, contentView.height - 32, 26, 26) + imageIcon.frame = CGRect(x: contentView.width - 32, y: contentView.height - 32, width: 26, height: 26) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/Cells/AAWallpapperSettingsCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/Cells/AAWallpapperSettingsCell.swift index 920bb797db..55ab87f8b0 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/Cells/AAWallpapperSettingsCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/Settings/Cells/AAWallpapperSettingsCell.swift @@ -5,41 +5,41 @@ import Foundation import YYImage -public class AAWallpapperSettingsCell: AATableViewCell { +open class AAWallpapperSettingsCell: AATableViewCell { - private let wallpapper1 = UIImageView() - private let wallpapper1Icon = UIImageView() - private let wallpapper2 = UIImageView() - private let wallpapper2Icon = UIImageView() - private let wallpapper3 = UIImageView() - private let wallpapper3Icon = UIImageView() - private let label = UILabel() - private let disclose = UIImageView() + fileprivate let wallpapper1 = UIImageView() + fileprivate let wallpapper1Icon = UIImageView() + fileprivate let wallpapper2 = UIImageView() + fileprivate let wallpapper2Icon = UIImageView() + fileprivate let wallpapper3 = UIImageView() + fileprivate let wallpapper3Icon = UIImageView() + fileprivate let label = UILabel() + fileprivate let disclose = UIImageView() - public var wallpapperDidTap: ((name: String) -> ())? + open var wallpapperDidTap: ((_ name: String) -> ())? public override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) wallpapper1.clipsToBounds = true - wallpapper1.contentMode = .ScaleAspectFill + wallpapper1.contentMode = .scaleAspectFill wallpapper1.image = UIImage.bundled("bg_1_preview.jpg")! - wallpapper1.viewDidTap = { [unowned self] () -> () in self.wallpapperDidTap?(name: "bg_1.jpg") } + wallpapper1.viewDidTap = { [unowned self] () -> () in self.wallpapperDidTap?("bg_1.jpg") } wallpapper1Icon.image = UIImage.bundled("ImageSelectedOn") wallpapper2.clipsToBounds = true - wallpapper2.contentMode = .ScaleAspectFill + wallpapper2.contentMode = .scaleAspectFill wallpapper2.image = UIImage.bundled("bg_2_preview.jpg")! - wallpapper2.viewDidTap = { [unowned self] () -> () in self.wallpapperDidTap?(name: "bg_2.jpg") } + wallpapper2.viewDidTap = { [unowned self] () -> () in self.wallpapperDidTap?("bg_2.jpg") } wallpapper2Icon.image = UIImage.bundled("ImageSelectedOn") wallpapper3.clipsToBounds = true - wallpapper3.contentMode = .ScaleAspectFill + wallpapper3.contentMode = .scaleAspectFill wallpapper3.image = UIImage.bundled("bg_3_preview.jpg")! - wallpapper3.viewDidTap = { [unowned self] () -> () in self.wallpapperDidTap?(name: "bg_3.jpg") } + wallpapper3.viewDidTap = { [unowned self] () -> () in self.wallpapperDidTap?("bg_3.jpg") } wallpapper3Icon.image = UIImage.bundled("ImageSelectedOn") - label.font = UIFont.systemFontOfSize(17) + label.font = UIFont.systemFont(ofSize: 17) label.textColor = appStyle.cellTextColor label.text = AALocalized("SettingsWallpapers") disclose.image = UIImage.bundled("ios_disclose") @@ -53,7 +53,7 @@ public class AAWallpapperSettingsCell: AATableViewCell { self.contentView.addSubview(label) self.contentView.addSubview(disclose) - disclose.hidden = false + disclose.isHidden = false //selectionStyle = .None } @@ -61,7 +61,7 @@ public class AAWallpapperSettingsCell: AATableViewCell { fatalError("init(coder:) has not been implemented") } - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() let width = contentView.width @@ -71,22 +71,22 @@ public class AAWallpapperSettingsCell: AATableViewCell { let wWidth = (width - padding * 4) / 3 let wHeight = height - padding - 44 - wallpapper1.frame = CGRectMake(padding, wPadding, wWidth, wHeight) - wallpapper2.frame = CGRectMake(padding * 2 + wWidth, wPadding, wWidth, wHeight) - wallpapper3.frame = CGRectMake(padding * 3 + wWidth * 2, wPadding, wWidth, wHeight) + wallpapper1.frame = CGRect(x: padding, y: wPadding, width: wWidth, height: wHeight) + wallpapper2.frame = CGRect(x: padding * 2 + wWidth, y: wPadding, width: wWidth, height: wHeight) + wallpapper3.frame = CGRect(x: padding * 3 + wWidth * 2, y: wPadding, width: wWidth, height: wHeight) - wallpapper1Icon.frame = CGRectMake(wWidth - 32, wHeight - 32, 26, 26) - wallpapper2Icon.frame = CGRectMake(wWidth - 32, wHeight - 32, 26, 26) - wallpapper3Icon.frame = CGRectMake(wWidth - 32, wHeight - 32, 26, 26) + wallpapper1Icon.frame = CGRect(x: wWidth - 32, y: wHeight - 32, width: 26, height: 26) + wallpapper2Icon.frame = CGRect(x: wWidth - 32, y: wHeight - 32, width: 26, height: 26) + wallpapper3Icon.frame = CGRect(x: wWidth - 32, y: wHeight - 32, width: 26, height: 26) - label.frame = CGRectMake(padding, 0, width - padding * 2, 44) + label.frame = CGRect(x: padding, y: 0, width: width - padding * 2, height: 44) - disclose.frame = CGRectMake(width - 13 - 10, 15, 13, 14) + disclose.frame = CGRect(x: width - 13 - 10, y: 15, width: 13, height: 14) } - public func bind() { - wallpapper1Icon.hidden = ActorSDK.sharedActor().messenger.getSelectedWallpaper() != "local:bg_1.jpg" - wallpapper2Icon.hidden = ActorSDK.sharedActor().messenger.getSelectedWallpaper() != "local:bg_2.jpg" - wallpapper3Icon.hidden = ActorSDK.sharedActor().messenger.getSelectedWallpaper() != "local:bg_3.jpg" + open func bind() { + wallpapper1Icon.isHidden = ActorSDK.sharedActor().messenger.getSelectedWallpaper() != "local:bg_1.jpg" + wallpapper2Icon.isHidden = ActorSDK.sharedActor().messenger.getSelectedWallpaper() != "local:bg_2.jpg" + wallpapper3Icon.isHidden = ActorSDK.sharedActor().messenger.getSelectedWallpaper() != "local:bg_3.jpg" } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/User/AAUserViewController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/User/AAUserViewController.swift index d322040fd1..f84797eb52 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/User/AAUserViewController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Controllers/User/AAUserViewController.swift @@ -10,7 +10,7 @@ class AAUserViewController: AAContentTableController { var isContactRow: AACommonRow! init(uid: Int) { - super.init(style: AAContentTableStyle.SettingsPlain) + super.init(style: AAContentTableStyle.settingsPlain) self.uid = uid self.autoTrack = true @@ -36,11 +36,11 @@ class AAUserViewController: AAContentTableController { r.avatar = self.user.getAvatarModel().get() let presence = self.user.getPresenceModel().get() - let presenceText = Actor.getFormatter().formatPresence(presence, withSex: self.user.getSex()) + let presenceText = Actor.getFormatter().formatPresence(presence, with: self.user.getSex()) if !self.isBot { r.subtitle = presenceText - if presence!.state.ordinal() == ACUserPresence_State.ONLINE().ordinal() { + if presence!.state.ordinal() == ACUserPresence_State.online().ordinal() { r.subtitleColor = self.appStyle.userOnlineColor } else { r.subtitleColor = self.appStyle.userOfflineColor @@ -53,13 +53,13 @@ class AAUserViewController: AAContentTableController { r.avatarDidTap = { [unowned self] (view: UIView) -> () in let avatar = self.user.getAvatarModel().get() - if avatar != nil && avatar.fullImage != nil { + if avatar != nil && avatar?.fullImage != nil { - let full = avatar.fullImage.fileReference - let small = avatar.smallImage.fileReference - let size = CGSize(width: Int(avatar.fullImage.width), height: Int(avatar.fullImage.height)) + let full = avatar?.fullImage.fileReference + let small = avatar?.smallImage.fileReference + let size = CGSize(width: Int((avatar?.fullImage.width)!), height: Int((avatar?.fullImage.height)!)) - self.presentViewController(AAPhotoPreviewController(file: full, previewFile: small, size: size, fromView: view), animated: true, completion: nil) + self.present(AAPhotoPreviewController(file: full!, previewFile: small, size: size, fromView: view), animated: true, completion: nil) } } } @@ -68,7 +68,7 @@ class AAUserViewController: AAContentTableController { // Profile: Starting Voice Call s.action("CallsStartAudio") { (r) -> () in r.selectAction = { () -> Bool in - self.execute(Actor.doCallWithUid(jint(self.uid))) + self.execute(Actor.doCall(withUid: jint(self.uid))) return false } } @@ -77,12 +77,12 @@ class AAUserViewController: AAContentTableController { // Profile: Send messages s.action("ProfileSendMessage") { (r) -> () in r.selectAction = { () -> Bool in - if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer.userWithInt(jint(self.uid))) { + if let customController = ActorSDK.sharedActor().delegate.actorControllerForConversation(ACPeer.user(with: jint(self.uid))) { self.navigateDetail(customController) } else { - self.navigateDetail(ConversationViewController(peer: ACPeer.userWithInt(jint(self.uid)))) + self.navigateDetail(ConversationViewController(peer: ACPeer.user(with: jint(self.uid)))) } - self.popover?.dismissPopoverAnimated(true) + self.popover?.dismiss(animated: true) return false } } @@ -113,9 +113,9 @@ class AAUserViewController: AAContentTableController { } r.selectAction = { (c: ACUserPhone) -> Bool in let phoneNumber = c.phone - let hasPhone = UIApplication.sharedApplication().canOpenURL(NSURL(string: "telprompt://")!) + let hasPhone = UIApplication.shared.canOpenURL(URL(string: "telprompt://")!) if (!hasPhone) { - UIPasteboard.generalPasteboard().string = "+\(phoneNumber)" + UIPasteboard.general.string = "+\(phoneNumber)" self.alertUser("NumberCopied") } else { ActorSDK.sharedActor().openUrl("telprompt://+\(phoneNumber)") @@ -149,44 +149,44 @@ class AAUserViewController: AAContentTableController { section { (s) -> () in s.common { (r) -> () in - let peer = ACPeer.userWithInt(jint(self.uid)) - r.style = .Switch + let peer = ACPeer.user(with: jint(self.uid)) + r.style = .switch r.content = AALocalized("ProfileNotifications") r.bindAction = { (r) -> () in - r.switchOn = Actor.isNotificationsEnabledWithPeer(peer) + r.switchOn = Actor.isNotificationsEnabled(with: peer) } r.switchAction = { (on: Bool) -> () in - if !on && !self.user.isBot().boolValue { + if !on && !self.user.isBot() { self.confirmAlertUser("ProfileNotificationsWarring", action: "ProfileNotificationsWarringAction", tapYes: { () -> () in - Actor.changeNotificationsEnabledWithPeer(peer, withValue: false) + Actor.changeNotificationsEnabled(with: peer, withValue: false) }, tapNo: { () -> () in r.reload() }) return } - Actor.changeNotificationsEnabledWithPeer(peer, withValue: on) + Actor.changeNotificationsEnabled(with: peer, withValue: on) } if(ActorSDK.sharedActor().enableChatGroupSound) { - if(Actor.isNotificationsEnabledWithPeer(peer)){ + if(Actor.isNotificationsEnabled(with: peer)){ r.selectAction = {() -> Bool in // Sound: Choose sound let setRingtoneController = AARingtonesViewController() - let sound = Actor.getNotificationsSoundWithPeer(peer) - setRingtoneController.selectedRingtone = (sound != nil) ? sound : "" + let sound = Actor.getNotificationsSound(with: peer) + setRingtoneController.selectedRingtone = (sound != nil) ? sound! : "" setRingtoneController.completion = {(selectedSound:String) in - Actor.changeNotificationsSoundPeer(peer, withValue: selectedSound) + Actor.changeNotificationsSound(peer, withValue: selectedSound) } let navigationController = AANavigationController(rootViewController: setRingtoneController) if (AADevice.isiPad) { - navigationController.modalInPopover = true - navigationController.modalPresentationStyle = UIModalPresentationStyle.CurrentContext + navigationController.isModalInPopover = true + navigationController.modalPresentationStyle = UIModalPresentationStyle.currentContext } - self.presentViewController(navigationController, animated: true, completion: { + self.present(navigationController, animated: true, completion: { } ) return false @@ -204,18 +204,18 @@ class AAUserViewController: AAContentTableController { r.bindAction = { (r) -> () in if self.user.isContactModel().get().booleanValue() { r.content = AALocalized("ProfileRemoveFromContacts") - r.style = .Destructive + r.style = .destructive } else { r.content = AALocalized("ProfileAddToContacts") - r.style = .Action + r.style = .action } } r.selectAction = { () -> Bool in if (self.user.isContactModel().get().booleanValue()) { - self.execute(Actor.removeContactCommandWithUid(jint(self.uid))!) + self.execute(Actor.removeContactCommand(withUid: jint(self.uid))!) } else { - self.execute(Actor.addContactCommandWithUid(jint(self.uid))!) + self.execute(Actor.addContactCommand(withUid: jint(self.uid))!) } return true } @@ -236,8 +236,8 @@ class AAUserViewController: AAContentTableController { if d.length == 0 { return } - c.executeSafeOnlySuccess(Actor.editNameCommandWithUid(jint(self.uid), withName: d)!, successBlock: { (val) -> Void in - c.dismiss() + c.executeSafeOnlySuccess(Actor.editNameCommand(withUid: jint(self.uid), withName: d)!, successBlock: { (val) -> Void in + c.dismissController() }) } } @@ -267,21 +267,21 @@ class AAUserViewController: AAContentTableController { } else { r.content = AALocalized("ProfileUnblockContact") } - r.style = .Destructive + r.style = .destructive } r.selectAction = { () -> Bool in if !self.user.isBlockedModel().get().booleanValue() { self.executePromise(Actor.blockUser(jint(self.uid)), successBlock: { success in - dispatch_async(dispatch_get_main_queue(),{ + DispatchQueue.main.async(execute: { r.reload() }) } ,failureBlock:nil) } else { self.executePromise(Actor.unblockUser(jint(self.uid)), successBlock: { success in - dispatch_async(dispatch_get_main_queue(),{ + DispatchQueue.main.async(execute: { r.reload() }) } ,failureBlock:nil) @@ -293,7 +293,7 @@ class AAUserViewController: AAContentTableController { } } - override func tableWillBind(binder: AABinder) { + override func tableWillBind(_ binder: AABinder) { binder.bind(user.getAvatarModel(), closure: { (value: ACAvatar?) -> () in self.headerRow.reload() }) diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ElegantPresentationController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ElegantPresentationController.swift index 1f2c5b4b1f..c73dfe4ca7 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ElegantPresentationController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ElegantPresentationController.swift @@ -8,7 +8,7 @@ import UIKit -typealias CoordinatedAnimation = UIViewControllerTransitionCoordinatorContext? -> Void +typealias CoordinatedAnimation = (UIViewControllerTransitionCoordinatorContext?) -> Void class ElegantPresentationController: UIPresentationController { @@ -16,22 +16,22 @@ class ElegantPresentationController: UIPresentationController { // MARK: - Properties /// Dims the presenting view controller, if option is set - private lazy var dimmingView: UIView = { + fileprivate lazy var dimmingView: UIView = { let view = UIView() - view.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.5) + view.backgroundColor = UIColor.black.withAlphaComponent(0.5) view.alpha = 0 - view.userInteractionEnabled = false + view.isUserInteractionEnabled = false return view }() /// For dismissing on tap if option is set - private lazy var recognizer: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: Selector("dismiss:")) + fileprivate lazy var recognizer: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ElegantPresentationController.dismiss(_:))) /// An options struct containing the customization options set - private let options: PresentationOptions + fileprivate let options: PresentationOptions // MARK: - Lifecycle @@ -47,7 +47,7 @@ class ElegantPresentationController: UIPresentationController { */ init(presentedViewController: UIViewController, presentingViewController: UIViewController, options: PresentationOptions) { self.options = options - super.init(presentedViewController: presentedViewController, presentingViewController: presentingViewController) + super.init(presentedViewController: presentedViewController, presenting: presentingViewController) } @@ -63,7 +63,7 @@ class ElegantPresentationController: UIPresentationController { // Prepare and position the dimming view dimmingView.alpha = 0 dimmingView.frame = containerView!.bounds - containerView?.insertSubview(dimmingView, atIndex: 0) + containerView?.insertSubview(dimmingView, at: 0) // Animate these properties with the transtion coordinator if possible let animations: CoordinatedAnimation = { [unowned self] _ in @@ -79,7 +79,7 @@ class ElegantPresentationController: UIPresentationController { // Animate these properties with the transtion coordinator if possible let animations: CoordinatedAnimation = { [unowned self] _ in self.dimmingView.alpha = 0 - self.presentingViewController.view.transform = CGAffineTransformIdentity + self.presentingViewController.view.transform = CGAffineTransform.identity } transtionWithCoordinator(animations) @@ -88,7 +88,7 @@ class ElegantPresentationController: UIPresentationController { // MARK: - Adaptation - override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) { + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { /* There's a bug when rotating that makes the presented view controller permanently @@ -97,19 +97,19 @@ class ElegantPresentationController: UIPresentationController { It jumps because it's not in the animation block, but isn't noticiable unless in slow-mo. Placing it in the animation block does not fix the issue, so here it is. */ - presentingViewController.view.transform = CGAffineTransformIdentity + presentingViewController.view.transform = CGAffineTransform.identity // Animate these with the coordinator let animations: CoordinatedAnimation = { [unowned self] _ in self.dimmingView.frame = self.containerView!.bounds self.presentingViewController.view.transform = self.options.presentingTransform - self.presentedView()?.frame = self.frameOfPresentedViewInContainerView() + self.presentedView?.frame = self.frameOfPresentedViewInContainerView } - coordinator.animateAlongsideTransition(animations, completion: nil) + coordinator.animate(alongsideTransition: animations, completion: nil) } - override func sizeForChildContentContainer(container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize { + override func size(forChildContentContainer container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize { // Percent height doesn't make sense as a negative value or greater than zero, so we'll enforce it let percentHeight = min(abs(options.presentedPercentHeight), 1) @@ -125,11 +125,11 @@ class ElegantPresentationController: UIPresentationController { return parentSize } - override func frameOfPresentedViewInContainerView() -> CGRect { + override var frameOfPresentedViewInContainerView : CGRect { // Grab the parent and child sizes let parentSize = containerView!.bounds.size - let childSize = sizeForChildContentContainer(presentedViewController, withParentContainerSize: parentSize) + let childSize = size(forChildContentContainer: presentedViewController, withParentContainerSize: parentSize) // Create and return an appropiate frame return CGRect(x: 0, y: parentSize.height - childSize.height, width: childSize.width, height: childSize.height) @@ -139,20 +139,20 @@ class ElegantPresentationController: UIPresentationController { // MARK: - Helper functions // For the tap-to-dismiss - func dismiss(sender: UITapGestureRecognizer) { - presentedViewController.dismissViewControllerAnimated(true, completion: nil) + func dismiss(_ sender: UITapGestureRecognizer) { + presentedViewController.dismiss(animated: true, completion: nil) } /* I noticed myself doing this a lot (more so in earlier versions) so I made a quick function. Simply takes a closure with animations in them and attempts to animate with the coordinator. */ - private func transtionWithCoordinator(animations: CoordinatedAnimation) { - if let coordinator = presentingViewController.transitionCoordinator() { - coordinator.animateAlongsideTransition(animations, completion: nil) + fileprivate func transtionWithCoordinator(_ animations: @escaping CoordinatedAnimation) { + if let coordinator = presentingViewController.transitionCoordinator { + coordinator.animate(alongsideTransition: animations, completion: nil) } else { animations(nil) } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ElegantPresentations.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ElegantPresentations.swift index 03b61965e6..2e70d505ab 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/ElegantPresentations.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/ElegantPresentations.swift @@ -37,7 +37,7 @@ public struct ElegantPresentations { struct PresentationOptions { var dimmingViewAlpha: CGFloat = 1 var dimmingViewTapDismisses = false - var presentingTransform = CGAffineTransformMakeScale(0.93, 0.93) + var presentingTransform = CGAffineTransform(scaleX: 0.93, y: 0.93) var presentedHeight: CGFloat = -1 var presentedPercentHeight = 1.0 var usePercentHeight = true @@ -47,18 +47,18 @@ struct PresentationOptions { init(options: Set) { for option in options { switch option { - case .NoDimmingView: dimmingViewAlpha = 0 - case .CustomDimmingViewAlpha(let alpha): dimmingViewAlpha = alpha - case .DismissOnDimmingViewTap: dimmingViewTapDismisses = true - case .PresentingViewKeepsSize: presentingTransform = CGAffineTransformIdentity - case .PresentedHeight(let height): + case .noDimmingView: dimmingViewAlpha = 0 + case .customDimmingViewAlpha(let alpha): dimmingViewAlpha = alpha + case .dismissOnDimmingViewTap: dimmingViewTapDismisses = true + case .presentingViewKeepsSize: presentingTransform = CGAffineTransform.identity + case .presentedHeight(let height): usePercentHeight = false presentedHeight = height - case .PresentedPercentHeight(let percentHeight): + case .presentedPercentHeight(let percentHeight): usePercentHeight = true presentedPercentHeight = percentHeight - case .CustomPresentingScale(let scale): - presentingTransform = CGAffineTransformMakeScale(CGFloat(min(1, scale)), CGFloat(min(1, scale))) + case .customPresentingScale(let scale): + presentingTransform = CGAffineTransform(scaleX: CGFloat(min(1, scale)), y: CGFloat(min(1, scale))) } } @@ -67,7 +67,7 @@ struct PresentationOptions { NSLog("\n-------------------------\nElegant Presentation Warning:\nDO NOT set a height and a percent height! Only one will be respected.\n-------------------------") } - if options.contains(.NoDimmingView) && dimmingViewAlpha != 0 { + if options.contains(.noDimmingView) && dimmingViewAlpha != 0 { NSLog("\n-------------------------\nElegant Presentation Warning:\nDO NOT set no dimming view and a custom dimming view alpha! Only one will be respected.\n-------------------------") } } @@ -84,23 +84,23 @@ struct PresentationOptions { */ public enum PresentationOption: Hashable { - case NoDimmingView - case CustomDimmingViewAlpha(CGFloat) - case DismissOnDimmingViewTap - case PresentingViewKeepsSize - case PresentedHeight(CGFloat) - case PresentedPercentHeight(Double) - case CustomPresentingScale(Double) + case noDimmingView + case customDimmingViewAlpha(CGFloat) + case dismissOnDimmingViewTap + case presentingViewKeepsSize + case presentedHeight(CGFloat) + case presentedPercentHeight(Double) + case customPresentingScale(Double) var description: String { switch self { - case .NoDimmingView: return "No dimming view" - case .CustomDimmingViewAlpha(let alpha): return "Custom dimming view alpha \(alpha)" - case .DismissOnDimmingViewTap: return "Dismiss on dimming view tap" - case .PresentingViewKeepsSize: return "Presenting view keeps size" - case .PresentedHeight(let height): return "Presented height \(height)" - case .PresentedPercentHeight(let percent): return "Presented percent height \(percent)" - case .CustomPresentingScale(let scale): return "Custom presenting scale \(scale)" + case .noDimmingView: return "No dimming view" + case .customDimmingViewAlpha(let alpha): return "Custom dimming view alpha \(alpha)" + case .dismissOnDimmingViewTap: return "Dismiss on dimming view tap" + case .presentingViewKeepsSize: return "Presenting view keeps size" + case .presentedHeight(let height): return "Presented height \(height)" + case .presentedPercentHeight(let percent): return "Presented percent height \(percent)" + case .customPresentingScale(let scale): return "Custom presenting scale \(scale)" } } @@ -111,4 +111,4 @@ public enum PresentationOption: Hashable { public func ==(lhs: PresentationOption, rhs: PresentationOption) -> Bool { return lhs.hashValue == rhs.hashValue -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/GCDAsyncSocket.h b/actor-sdk/sdk-core-ios/ActorSDK/Sources/GCDAsyncSocket.h new file mode 100755 index 0000000000..6c4f04095c --- /dev/null +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/GCDAsyncSocket.h @@ -0,0 +1,1210 @@ +// +// GCDAsyncSocket.h +// +// This class is in the public domain. +// Originally created by Robbie Hanson in Q3 2010. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import +#import +#import +#import +#import + +#include // AF_INET, AF_INET6 + +@class GCDAsyncReadPacket; +@class GCDAsyncWritePacket; +@class GCDAsyncSocketPreBuffer; +@protocol GCDAsyncSocketDelegate; + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const GCDAsyncSocketException; +extern NSString *const GCDAsyncSocketErrorDomain; + +extern NSString *const GCDAsyncSocketQueueName; +extern NSString *const GCDAsyncSocketThreadName; + +extern NSString *const GCDAsyncSocketManuallyEvaluateTrust; +#if TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketUseCFStreamForTLS; +#endif +#define GCDAsyncSocketSSLPeerName (NSString *)kCFStreamSSLPeerName +#define GCDAsyncSocketSSLCertificates (NSString *)kCFStreamSSLCertificates +#define GCDAsyncSocketSSLIsServer (NSString *)kCFStreamSSLIsServer +extern NSString *const GCDAsyncSocketSSLPeerID; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMin; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMax; +extern NSString *const GCDAsyncSocketSSLSessionOptionFalseStart; +extern NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord; +extern NSString *const GCDAsyncSocketSSLCipherSuites; +#if !TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketSSLDiffieHellmanParameters; +#endif + +#define GCDAsyncSocketLoggingContext 65535 + + +typedef NS_ENUM(NSInteger, GCDAsyncSocketError) { + GCDAsyncSocketNoError = 0, // Never used + GCDAsyncSocketBadConfigError, // Invalid configuration + GCDAsyncSocketBadParamError, // Invalid parameter was passed + GCDAsyncSocketConnectTimeoutError, // A connect operation timed out + GCDAsyncSocketReadTimeoutError, // A read operation timed out + GCDAsyncSocketWriteTimeoutError, // A write operation timed out + GCDAsyncSocketReadMaxedOutError, // Reached set maxLength without completing + GCDAsyncSocketClosedError, // The remote peer closed the connection + GCDAsyncSocketOtherError, // Description provided in userInfo +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +@interface GCDAsyncSocket : NSObject + +/** + * GCDAsyncSocket uses the standard delegate paradigm, + * but executes all delegate callbacks on a given delegate dispatch queue. + * This allows for maximum concurrency, while at the same time providing easy thread safety. + * + * You MUST set a delegate AND delegate dispatch queue before attempting to + * use the socket, or you will get an error. + * + * The socket queue is optional. + * If you pass NULL, GCDAsyncSocket will automatically create it's own socket queue. + * If you choose to provide a socket queue, the socket queue must not be a concurrent queue. + * If you choose to provide a socket queue, and the socket queue has a configured target queue, + * then please see the discussion for the method markSocketQueueTargetQueue. + * + * The delegate queue and socket queue can optionally be the same. +**/ +- (instancetype)init; +- (instancetype)initWithSocketQueue:(nullable dispatch_queue_t)sq; +- (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq; +- (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq; + +#pragma mark Configuration + +@property (atomic, weak, readwrite, nullable) id delegate; +#if OS_OBJECT_USE_OBJC +@property (atomic, strong, readwrite, nullable) dispatch_queue_t delegateQueue; +#else +@property (atomic, assign, readwrite, nullable) dispatch_queue_t delegateQueue; +#endif + +- (void)getDelegate:(id __nullable * __nullable)delegatePtr delegateQueue:(dispatch_queue_t __nullable * __nullable)delegateQueuePtr; +- (void)setDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; + +/** + * If you are setting the delegate to nil within the delegate's dealloc method, + * you may need to use the synchronous versions below. +**/ +- (void)synchronouslySetDelegate:(nullable id)delegate; +- (void)synchronouslySetDelegateQueue:(nullable dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; + +/** + * By default, both IPv4 and IPv6 are enabled. + * + * For accepting incoming connections, this means GCDAsyncSocket automatically supports both protocols, + * and can simulataneously accept incoming connections on either protocol. + * + * For outgoing connections, this means GCDAsyncSocket can connect to remote hosts running either protocol. + * If a DNS lookup returns only IPv4 results, GCDAsyncSocket will automatically use IPv4. + * If a DNS lookup returns only IPv6 results, GCDAsyncSocket will automatically use IPv6. + * If a DNS lookup returns both IPv4 and IPv6 results, the preferred protocol will be chosen. + * By default, the preferred protocol is IPv4, but may be configured as desired. +**/ + +@property (atomic, assign, readwrite, getter=isIPv4Enabled) BOOL IPv4Enabled; +@property (atomic, assign, readwrite, getter=isIPv6Enabled) BOOL IPv6Enabled; + +@property (atomic, assign, readwrite, getter=isIPv4PreferredOverIPv6) BOOL IPv4PreferredOverIPv6; + +/** + * When connecting to both IPv4 and IPv6 using Happy Eyeballs (RFC 6555) https://tools.ietf.org/html/rfc6555 + * this is the delay between connecting to the preferred protocol and the fallback protocol. + * + * Defaults to 300ms. +**/ +@property (atomic, assign, readwrite) NSTimeInterval alternateAddressDelay; + +/** + * User data allows you to associate arbitrary information with the socket. + * This data is not used internally by socket in any way. +**/ +@property (atomic, strong, readwrite, nullable) id userData; + +#pragma mark Accepting + +/** + * Tells the socket to begin listening and accepting connections on the given port. + * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, + * and the socket:didAcceptNewSocket: delegate method will be invoked. + * + * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) +**/ +- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * This method is the same as acceptOnPort:error: with the + * additional option of specifying which interface to listen on. + * + * For example, you could specify that the socket should only accept connections over ethernet, + * and not other interfaces such as wifi. + * + * The interface may be specified by name (e.g. "en1" or "lo0") or by IP address (e.g. "192.168.4.34"). + * You may also use the special strings "localhost" or "loopback" to specify that + * the socket only accept connections from the local machine. + * + * You can see the list of interfaces via the command line utility "ifconfig", + * or programmatically via the getifaddrs() function. + * + * To accept connections on any interface pass nil, or simply use the acceptOnPort:error: method. +**/ +- (BOOL)acceptOnInterface:(nullable NSString *)interface port:(uint16_t)port error:(NSError **)errPtr; + +/** + * Tells the socket to begin listening and accepting connections on the unix domain at the given url. + * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, + * and the socket:didAcceptNewSocket: delegate method will be invoked. + * + * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) + **/ +- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr; + +#pragma mark Connecting + +/** + * Connects to the given host and port. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: + * and uses the default interface, and no timeout. +**/ +- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Connects to the given host and port with an optional timeout. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: and uses the default interface. +**/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given host & port, via the optional interface, with an optional timeout. + * + * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * The host may also be the special strings "localhost" or "loopback" to specify connecting + * to a service on the local machine. + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. +**/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + viaInterface:(nullable NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given address, specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * This method invokes connectToAdd +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; + +/** + * This method is the same as connectToAddress:error: with an additional timeout option. + * To not time out use a negative time interval, or simply use the connectToAddress:error: method. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +/** + * Connects to the given address, using the specified interface and timeout. + * + * The address is specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * The timeout is optional. To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr + viaInterface:(nullable NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; +/** + * Connects to the unix domain socket at the given url, using the specified timeout. + */ +- (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +#pragma mark Disconnecting + +/** + * Disconnects immediately (synchronously). Any pending reads or writes are dropped. + * + * If the socket is not already disconnected, an invocation to the socketDidDisconnect:withError: delegate method + * will be queued onto the delegateQueue asynchronously (behind any previously queued delegate methods). + * In other words, the disconnected delegate method will be invoked sometime shortly after this method returns. + * + * Please note the recommended way of releasing a GCDAsyncSocket instance (e.g. in a dealloc method) + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket release]; + * + * If you plan on disconnecting the socket, and then immediately asking it to connect again, + * you'll likely want to do so like this: + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket setDelegate:self]; + * [asyncSocket connect...]; +**/ +- (void)disconnect; + +/** + * Disconnects after all pending reads have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending writes. +**/ +- (void)disconnectAfterReading; + +/** + * Disconnects after all pending writes have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending reads. +**/ +- (void)disconnectAfterWriting; + +/** + * Disconnects after all pending reads and writes have completed. + * After calling this, the read and write methods will do nothing. +**/ +- (void)disconnectAfterReadingAndWriting; + +#pragma mark Diagnostics + +/** + * Returns whether the socket is disconnected or connected. + * + * A disconnected socket may be recycled. + * That is, it can used again for connecting or listening. + * + * If a socket is in the process of connecting, it may be neither disconnected nor connected. +**/ +@property (atomic, readonly) BOOL isDisconnected; +@property (atomic, readonly) BOOL isConnected; + +/** + * Returns the local or remote host and port to which this socket is connected, or nil and 0 if not connected. + * The host will be an IP address. +**/ +@property (atomic, readonly, nullable) NSString *connectedHost; +@property (atomic, readonly) uint16_t connectedPort; +@property (atomic, readonly, nullable) NSURL *connectedUrl; + +@property (atomic, readonly, nullable) NSString *localHost; +@property (atomic, readonly) uint16_t localPort; + +/** + * Returns the local or remote address to which this socket is connected, + * specified as a sockaddr structure wrapped in a NSData object. + * + * @seealso connectedHost + * @seealso connectedPort + * @seealso localHost + * @seealso localPort +**/ +@property (atomic, readonly, nullable) NSData *connectedAddress; +@property (atomic, readonly, nullable) NSData *localAddress; + +/** + * Returns whether the socket is IPv4 or IPv6. + * An accepting socket may be both. +**/ +@property (atomic, readonly) BOOL isIPv4; +@property (atomic, readonly) BOOL isIPv6; + +/** + * Returns whether or not the socket has been secured via SSL/TLS. + * + * See also the startTLS method. +**/ +@property (atomic, readonly) BOOL isSecure; + +#pragma mark Reading + +// The readData and writeData methods won't block (they are asynchronous). +// +// When a read is complete the socket:didReadData:withTag: delegate method is dispatched on the delegateQueue. +// When a write is complete the socket:didWriteDataWithTag: delegate method is dispatched on the delegateQueue. +// +// You may optionally set a timeout for any read/write operation. (To not timeout, use a negative time interval.) +// If a read/write opertion times out, the corresponding "socket:shouldTimeout..." delegate method +// is called to optionally allow you to extend the timeout. +// Upon a timeout, the "socket:didDisconnectWithError:" method is called +// +// The tag is for your convenience. +// You can use it as an array index, step number, state id, pointer, etc. + +/** + * Reads the first available bytes that become available on the socket. + * + * If the timeout value is negative, the read operation will not use a timeout. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, the socket will create a buffer for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * A maximum of length bytes will be read. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * If maxLength is zero, no length restriction is enforced. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Reads the given number of bytes. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If the length is 0, this method does nothing and the delegate is not called. +**/ +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the given number of bytes. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If the length is 0, this method does nothing and the delegate is not called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If you pass a maxLength parameter that is less than the length of the data parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass a maxLength parameter that is less than the length of the data (separator) parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Returns progress of the current read, from 0.0 to 1.0, or NaN if no current read (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfReadReturningTag:(nullable long *)tagPtr bytesDone:(nullable NSUInteger *)donePtr total:(nullable NSUInteger *)totalPtr; + +#pragma mark Writing + +/** + * Writes data to the socket, and calls the delegate when finished. + * + * If you pass in nil or zero-length data, this method does nothing and the delegate will not be called. + * If the timeout value is negative, the write operation will not use a timeout. + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is writing it. In other words, it's not safe to alter the data until after the delegate method + * socket:didWriteDataWithTag: is invoked signifying that this particular write operation has completed. + * This is due to the fact that GCDAsyncSocket does NOT copy the data. It simply retains it. + * This is for performance reasons. Often times, if NSMutableData is passed, it is because + * a request/response was built up in memory. Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes writing the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Returns progress of the current write, from 0.0 to 1.0, or NaN if no current write (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfWriteReturningTag:(nullable long *)tagPtr bytesDone:(nullable NSUInteger *)donePtr total:(nullable NSUInteger *)totalPtr; + +#pragma mark Security + +/** + * Secures the connection using SSL/TLS. + * + * This method may be called at any time, and the TLS handshake will occur after all pending reads and writes + * are finished. This allows one the option of sending a protocol dependent StartTLS message, and queuing + * the upgrade to TLS at the same time, without having to wait for the write to finish. + * Any reads or writes scheduled after this method is called will occur over the secured connection. + * + * ==== The available TOP-LEVEL KEYS are: + * + * - GCDAsyncSocketManuallyEvaluateTrust + * The value must be of type NSNumber, encapsulating a BOOL value. + * If you set this to YES, then the underlying SecureTransport system will not evaluate the SecTrustRef of the peer. + * Instead it will pause at the moment evaulation would typically occur, + * and allow us to handle the security evaluation however we see fit. + * So GCDAsyncSocket will invoke the delegate method socket:shouldTrustPeer: passing the SecTrustRef. + * + * Note that if you set this option, then all other configuration keys are ignored. + * Evaluation will be completely up to you during the socket:didReceiveTrust:completionHandler: delegate method. + * + * For more information on trust evaluation see: + * Apple's Technical Note TN2232 - HTTPS Server Trust Evaluation + * https://developer.apple.com/library/ios/technotes/tn2232/_index.html + * + * If unspecified, the default value is NO. + * + * - GCDAsyncSocketUseCFStreamForTLS (iOS only) + * The value must be of type NSNumber, encapsulating a BOOL value. + * By default GCDAsyncSocket will use the SecureTransport layer to perform encryption. + * This gives us more control over the security protocol (many more configuration options), + * plus it allows us to optimize things like sys calls and buffer allocation. + * + * However, if you absolutely must, you can instruct GCDAsyncSocket to use the old-fashioned encryption + * technique by going through the CFStream instead. So instead of using SecureTransport, GCDAsyncSocket + * will instead setup a CFRead/CFWriteStream. And then set the kCFStreamPropertySSLSettings property + * (via CFReadStreamSetProperty / CFWriteStreamSetProperty) and will pass the given options to this method. + * + * Thus all the other keys in the given dictionary will be ignored by GCDAsyncSocket, + * and will passed directly CFReadStreamSetProperty / CFWriteStreamSetProperty. + * For more infomation on these keys, please see the documentation for kCFStreamPropertySSLSettings. + * + * If unspecified, the default value is NO. + * + * ==== The available CONFIGURATION KEYS are: + * + * - kCFStreamSSLPeerName + * The value must be of type NSString. + * It should match the name in the X.509 certificate given by the remote party. + * See Apple's documentation for SSLSetPeerDomainName. + * + * - kCFStreamSSLCertificates + * The value must be of type NSArray. + * See Apple's documentation for SSLSetCertificate. + * + * - kCFStreamSSLIsServer + * The value must be of type NSNumber, encapsulationg a BOOL value. + * See Apple's documentation for SSLCreateContext for iOS. + * This is optional for iOS. If not supplied, a NO value is the default. + * This is not needed for Mac OS X, and the value is ignored. + * + * - GCDAsyncSocketSSLPeerID + * The value must be of type NSData. + * You must set this value if you want to use TLS session resumption. + * See Apple's documentation for SSLSetPeerID. + * + * - GCDAsyncSocketSSLProtocolVersionMin + * - GCDAsyncSocketSSLProtocolVersionMax + * The value(s) must be of type NSNumber, encapsulting a SSLProtocol value. + * See Apple's documentation for SSLSetProtocolVersionMin & SSLSetProtocolVersionMax. + * See also the SSLProtocol typedef. + * + * - GCDAsyncSocketSSLSessionOptionFalseStart + * The value must be of type NSNumber, encapsulating a BOOL value. + * See Apple's documentation for kSSLSessionOptionFalseStart. + * + * - GCDAsyncSocketSSLSessionOptionSendOneByteRecord + * The value must be of type NSNumber, encapsulating a BOOL value. + * See Apple's documentation for kSSLSessionOptionSendOneByteRecord. + * + * - GCDAsyncSocketSSLCipherSuites + * The values must be of type NSArray. + * Each item within the array must be a NSNumber, encapsulating + * See Apple's documentation for SSLSetEnabledCiphers. + * See also the SSLCipherSuite typedef. + * + * - GCDAsyncSocketSSLDiffieHellmanParameters (Mac OS X only) + * The value must be of type NSData. + * See Apple's documentation for SSLSetDiffieHellmanParams. + * + * ==== The following UNAVAILABLE KEYS are: (with throw an exception) + * + * - kCFStreamSSLAllowsAnyRoot (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsAnyRoot + * + * - kCFStreamSSLAllowsExpiredRoots (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsExpiredRoots + * + * - kCFStreamSSLAllowsExpiredCertificates (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsExpiredCerts + * + * - kCFStreamSSLValidatesCertificateChain (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetEnableCertVerify + * + * - kCFStreamSSLLevel (UNAVAILABLE) + * You MUST use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMin instead. + * Corresponding deprecated method: SSLSetProtocolVersionEnabled + * + * + * Please refer to Apple's documentation for corresponding SSLFunctions. + * + * If you pass in nil or an empty dictionary, the default settings will be used. + * + * IMPORTANT SECURITY NOTE: + * The default settings will check to make sure the remote party's certificate is signed by a + * trusted 3rd party certificate agency (e.g. verisign) and that the certificate is not expired. + * However it will not verify the name on the certificate unless you + * give it a name to verify against via the kCFStreamSSLPeerName key. + * The security implications of this are important to understand. + * Imagine you are attempting to create a secure connection to MySecureServer.com, + * but your socket gets directed to MaliciousServer.com because of a hacked DNS server. + * If you simply use the default settings, and MaliciousServer.com has a valid certificate, + * the default settings will not detect any problems since the certificate is valid. + * To properly secure your connection in this particular scenario you + * should set the kCFStreamSSLPeerName property to "MySecureServer.com". + * + * You can also perform additional validation in socketDidSecure. +**/ +- (void)startTLS:(nullable NSDictionary *)tlsSettings; + +#pragma mark Advanced + +/** + * Traditionally sockets are not closed until the conversation is over. + * However, it is technically possible for the remote enpoint to close its write stream. + * Our socket would then be notified that there is no more data to be read, + * but our socket would still be writeable and the remote endpoint could continue to receive our data. + * + * The argument for this confusing functionality stems from the idea that a client could shut down its + * write stream after sending a request to the server, thus notifying the server there are to be no further requests. + * In practice, however, this technique did little to help server developers. + * + * To make matters worse, from a TCP perspective there is no way to tell the difference from a read stream close + * and a full socket close. They both result in the TCP stack receiving a FIN packet. The only way to tell + * is by continuing to write to the socket. If it was only a read stream close, then writes will continue to work. + * Otherwise an error will be occur shortly (when the remote end sends us a RST packet). + * + * In addition to the technical challenges and confusion, many high level socket/stream API's provide + * no support for dealing with the problem. If the read stream is closed, the API immediately declares the + * socket to be closed, and shuts down the write stream as well. In fact, this is what Apple's CFStream API does. + * It might sound like poor design at first, but in fact it simplifies development. + * + * The vast majority of the time if the read stream is closed it's because the remote endpoint closed its socket. + * Thus it actually makes sense to close the socket at this point. + * And in fact this is what most networking developers want and expect to happen. + * However, if you are writing a server that interacts with a plethora of clients, + * you might encounter a client that uses the discouraged technique of shutting down its write stream. + * If this is the case, you can set this property to NO, + * and make use of the socketDidCloseReadStream delegate method. + * + * The default value is YES. +**/ +@property (atomic, assign, readwrite) BOOL autoDisconnectOnClosedReadStream; + +/** + * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue. + * In most cases, the instance creates this queue itself. + * However, to allow for maximum flexibility, the internal queue may be passed in the init method. + * This allows for some advanced options such as controlling socket priority via target queues. + * However, when one begins to use target queues like this, they open the door to some specific deadlock issues. + * + * For example, imagine there are 2 queues: + * dispatch_queue_t socketQueue; + * dispatch_queue_t socketTargetQueue; + * + * If you do this (pseudo-code): + * socketQueue.targetQueue = socketTargetQueue; + * + * Then all socketQueue operations will actually get run on the given socketTargetQueue. + * This is fine and works great in most situations. + * But if you run code directly from within the socketTargetQueue that accesses the socket, + * you could potentially get deadlock. Imagine the following code: + * + * - (BOOL)socketHasSomething + * { + * __block BOOL result = NO; + * dispatch_block_t block = ^{ + * result = [self someInternalMethodToBeRunOnlyOnSocketQueue]; + * } + * if (is_executing_on_queue(socketQueue)) + * block(); + * else + * dispatch_sync(socketQueue, block); + * + * return result; + * } + * + * What happens if you call this method from the socketTargetQueue? The result is deadlock. + * This is because the GCD API offers no mechanism to discover a queue's targetQueue. + * Thus we have no idea if our socketQueue is configured with a targetQueue. + * If we had this information, we could easily avoid deadlock. + * But, since these API's are missing or unfeasible, you'll have to explicitly set it. + * + * IF you pass a socketQueue via the init method, + * AND you've configured the passed socketQueue with a targetQueue, + * THEN you should pass the end queue in the target hierarchy. + * + * For example, consider the following queue hierarchy: + * socketQueue -> ipQueue -> moduleQueue + * + * This example demonstrates priority shaping within some server. + * All incoming client connections from the same IP address are executed on the same target queue. + * And all connections for a particular module are executed on the same target queue. + * Thus, the priority of all networking for the entire module can be changed on the fly. + * Additionally, networking traffic from a single IP cannot monopolize the module. + * + * Here's how you would accomplish something like that: + * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock + * { + * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL); + * dispatch_queue_t ipQueue = [self ipQueueForAddress:address]; + * + * dispatch_set_target_queue(socketQueue, ipQueue); + * dispatch_set_target_queue(iqQueue, moduleQueue); + * + * return socketQueue; + * } + * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket + * { + * [clientConnections addObject:newSocket]; + * [newSocket markSocketQueueTargetQueue:moduleQueue]; + * } + * + * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue. + * This is often NOT the case, as such queues are used solely for execution shaping. +**/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue; +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue; + +/** + * It's not thread-safe to access certain variables from outside the socket's internal queue. + * + * For example, the socket file descriptor. + * File descriptors are simply integers which reference an index in the per-process file table. + * However, when one requests a new file descriptor (by opening a file or socket), + * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor. + * So if we're not careful, the following could be possible: + * + * - Thread A invokes a method which returns the socket's file descriptor. + * - The socket is closed via the socket's internal queue on thread B. + * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD. + * - Thread A is now accessing/altering the file instead of the socket. + * + * In addition to this, other variables are not actually objects, + * and thus cannot be retained/released or even autoreleased. + * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct. + * + * Although there are internal variables that make it difficult to maintain thread-safety, + * it is important to provide access to these variables + * to ensure this class can be used in a wide array of environments. + * This method helps to accomplish this by invoking the current block on the socket's internal queue. + * The methods below can be invoked from within the block to access + * those generally thread-unsafe internal variables in a thread-safe manner. + * The given block will be invoked synchronously on the socket's internal queue. + * + * If you save references to any protected variables and use them outside the block, you do so at your own peril. +**/ +- (void)performBlock:(dispatch_block_t)block; + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's file descriptor(s). + * If the socket is a server socket (is accepting incoming connections), + * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6. +**/ +- (int)socketFD; +- (int)socket4FD; +- (int)socket6FD; + +#if TARGET_OS_IPHONE + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's internal CFReadStream/CFWriteStream. + * + * These streams are only used as workarounds for specific iOS shortcomings: + * + * - Apple has decided to keep the SecureTransport framework private is iOS. + * This means the only supplied way to do SSL/TLS is via CFStream or some other API layered on top of it. + * Thus, in order to provide SSL/TLS support on iOS we are forced to rely on CFStream, + * instead of the preferred and faster and more powerful SecureTransport. + * + * - If a socket doesn't have backgrounding enabled, and that socket is closed while the app is backgrounded, + * Apple only bothers to notify us via the CFStream API. + * The faster and more powerful GCD API isn't notified properly in this case. + * + * See also: (BOOL)enableBackgroundingOnSocket +**/ +- (nullable CFReadStreamRef)readStream; +- (nullable CFWriteStreamRef)writeStream; + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Configures the socket to allow it to operate when the iOS application has been backgrounded. + * In other words, this method creates a read & write stream, and invokes: + * + * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * + * Returns YES if successful, NO otherwise. + * + * Note: Apple does not officially support backgrounding server sockets. + * That is, if your socket is accepting incoming connections, Apple does not officially support + * allowing iOS applications to accept incoming connections while an app is backgrounded. + * + * Example usage: + * + * - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port + * { + * [asyncSocket performBlock:^{ + * [asyncSocket enableBackgroundingOnSocket]; + * }]; + * } +**/ +- (BOOL)enableBackgroundingOnSocket; + +#endif + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's SSLContext, if SSL/TLS has been started on the socket. +**/ +- (nullable SSLContextRef)sslContext; + +#pragma mark Utilities + +/** + * The address lookup utility used by the class. + * This method is synchronous, so it's recommended you use it on a background thread/queue. + * + * The special strings "localhost" and "loopback" return the loopback address for IPv4 and IPv6. + * + * @returns + * A mutable array with all IPv4 and IPv6 addresses returned by getaddrinfo. + * The addresses are specifically for TCP connections. + * You can filter the addresses, if needed, using the other utility methods provided by the class. +**/ ++ (nullable NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr; + +/** + * Extracting host and port information from raw address data. +**/ + ++ (nullable NSString *)hostFromAddress:(NSData *)address; ++ (uint16_t)portFromAddress:(NSData *)address; + ++ (BOOL)isIPv4Address:(NSData *)address; ++ (BOOL)isIPv6Address:(NSData *)address; + ++ (BOOL)getHost:( NSString * __nullable * __nullable)hostPtr port:(nullable uint16_t *)portPtr fromAddress:(NSData *)address; + ++ (BOOL)getHost:(NSString * __nullable * __nullable)hostPtr port:(nullable uint16_t *)portPtr family:(nullable sa_family_t *)afPtr fromAddress:(NSData *)address; + +/** + * A few common line separators, for use with the readDataToData:... methods. +**/ ++ (NSData *)CRLFData; // 0x0D0A ++ (NSData *)CRData; // 0x0D ++ (NSData *)LFData; // 0x0A ++ (NSData *)ZeroData; // 0x00 + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol GCDAsyncSocketDelegate +@optional + +/** + * This method is called immediately prior to socket:didAcceptNewSocket:. + * It optionally allows a listening socket to specify the socketQueue for a new accepted socket. + * If this method is not implemented, or returns NULL, the new accepted socket will create its own default queue. + * + * Since you cannot autorelease a dispatch_queue, + * this method uses the "new" prefix in its name to specify that the returned queue has been retained. + * + * Thus you could do something like this in the implementation: + * return dispatch_queue_create("MyQueue", NULL); + * + * If you are placing multiple sockets on the same queue, + * then care should be taken to increment the retain count each time this method is invoked. + * + * For example, your implementation might look something like this: + * dispatch_retain(myExistingQueue); + * return myExistingQueue; +**/ +- (nullable dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock; + +/** + * Called when a socket accepts a connection. + * Another socket is automatically spawned to handle it. + * + * You must retain the newSocket if you wish to handle the connection. + * Otherwise the newSocket instance will be released and the spawned connection will be closed. + * + * By default the new socket will have the same delegate and delegateQueue. + * You may, of course, change this at any time. +**/ +- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. +**/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. + **/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToUrl:(NSURL *)url; + +/** + * Called when a socket has completed reading the requested data into memory. + * Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag; + +/** + * Called when a socket has read in data, but has not yet completed the read. + * This would occur if using readToData: or readToLength: methods. + * It may be used to for things such as updating progress bars. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called when a socket has completed writing the requested data. Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag; + +/** + * Called when a socket has written some data, but has not yet completed the entire write. + * It may be used to for things such as updating progress bars. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called if a read operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the read's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the read will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been read so far for the read operation. + * + * Note that this method may be called multiple times for a single read if you return positive numbers. +**/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Called if a write operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the write's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the write will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been written so far for the write operation. + * + * Note that this method may be called multiple times for a single write if you return positive numbers. +**/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutWriteWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Conditionally called if the read stream closes, but the write stream may still be writeable. + * + * This delegate method is only called if autoDisconnectOnClosedReadStream has been set to NO. + * See the discussion on the autoDisconnectOnClosedReadStream method for more information. +**/ +- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock; + +/** + * Called when a socket disconnects with or without error. + * + * If you call the disconnect method, and the socket wasn't already disconnected, + * then an invocation of this delegate method will be enqueued on the delegateQueue + * before the disconnect method returns. + * + * Note: If the GCDAsyncSocket instance is deallocated while it is still connected, + * and the delegate is not also deallocated, then this method will be invoked, + * but the sock parameter will be nil. (It must necessarily be nil since it is no longer available.) + * This is a generally rare, but is possible if one writes code like this: + * + * asyncSocket = nil; // I'm implicitly disconnecting the socket + * + * In this case it may preferrable to nil the delegate beforehand, like this: + * + * asyncSocket.delegate = nil; // Don't invoke my delegate method + * asyncSocket = nil; // I'm implicitly disconnecting the socket + * + * Of course, this depends on how your state machine is configured. +**/ +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err; + +/** + * Called after the socket has successfully completed SSL/TLS negotiation. + * This method is not called unless you use the provided startTLS method. + * + * If a SSL/TLS negotiation fails (invalid certificate, etc) then the socket will immediately close, + * and the socketDidDisconnect:withError: delegate method will be called with the specific SSL error code. +**/ +- (void)socketDidSecure:(GCDAsyncSocket *)sock; + +/** + * Allows a socket delegate to hook into the TLS handshake and manually validate the peer it's connecting to. + * + * This is only called if startTLS is invoked with options that include: + * - GCDAsyncSocketManuallyEvaluateTrust == YES + * + * Typically the delegate will use SecTrustEvaluate (and related functions) to properly validate the peer. + * + * Note from Apple's documentation: + * Because [SecTrustEvaluate] might look on the network for certificates in the certificate chain, + * [it] might block while attempting network access. You should never call it from your main thread; + * call it only from within a function running on a dispatch queue or on a separate thread. + * + * Thus this method uses a completionHandler block rather than a normal return value. + * The completionHandler block is thread-safe, and may be invoked from a background queue/thread. + * It is safe to invoke the completionHandler block even if the socket has been closed. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust + completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler; + +@end +NS_ASSUME_NONNULL_END diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/GCDAsyncSocket.m b/actor-sdk/sdk-core-ios/ActorSDK/Sources/GCDAsyncSocket.m new file mode 100755 index 0000000000..3546d105b7 --- /dev/null +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/GCDAsyncSocket.m @@ -0,0 +1,8365 @@ +// +// GCDAsyncSocket.m +// +// This class is in the public domain. +// Originally created by Robbie Hanson in Q4 2010. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import "GCDAsyncSocket.h" + +#if TARGET_OS_IPHONE +#import +#endif + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +// For more information see: https://github.com/robbiehanson/CocoaAsyncSocket/wiki/ARC +#endif + + +#ifndef GCDAsyncSocketLoggingEnabled +#define GCDAsyncSocketLoggingEnabled 0 +#endif + +#if GCDAsyncSocketLoggingEnabled + +// Logging Enabled - See log level below + +// Logging uses the CocoaLumberjack framework (which is also GCD based). +// https://github.com/robbiehanson/CocoaLumberjack +// +// It allows us to do a lot of logging without significantly slowing down the code. +#import "DDLog.h" + +#define LogAsync YES +#define LogContext GCDAsyncSocketLoggingContext + +#define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) +#define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) + +#define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD) +#define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__) + +#ifndef GCDAsyncSocketLogLevel +#define GCDAsyncSocketLogLevel LOG_LEVEL_VERBOSE +#endif + +// Log levels : off, error, warn, info, verbose +static const int logLevel = GCDAsyncSocketLogLevel; + +#else + +// Logging Disabled + +#define LogError(frmt, ...) {} +#define LogWarn(frmt, ...) {} +#define LogInfo(frmt, ...) {} +#define LogVerbose(frmt, ...) {} + +#define LogCError(frmt, ...) {} +#define LogCWarn(frmt, ...) {} +#define LogCInfo(frmt, ...) {} +#define LogCVerbose(frmt, ...) {} + +#define LogTrace() {} +#define LogCTrace(frmt, ...) {} + +#endif + +/** + * Seeing a return statements within an inner block + * can sometimes be mistaken for a return point of the enclosing method. + * This makes inline blocks a bit easier to read. +**/ +#define return_from_block return + +/** + * A socket file descriptor is really just an integer. + * It represents the index of the socket within the kernel. + * This makes invalid file descriptor comparisons easier to read. +**/ +#define SOCKET_NULL -1 + + +NSString *const GCDAsyncSocketException = @"GCDAsyncSocketException"; +NSString *const GCDAsyncSocketErrorDomain = @"GCDAsyncSocketErrorDomain"; + +NSString *const GCDAsyncSocketQueueName = @"GCDAsyncSocket"; +NSString *const GCDAsyncSocketThreadName = @"GCDAsyncSocket-CFStream"; + +NSString *const GCDAsyncSocketManuallyEvaluateTrust = @"GCDAsyncSocketManuallyEvaluateTrust"; +#if TARGET_OS_IPHONE +NSString *const GCDAsyncSocketUseCFStreamForTLS = @"GCDAsyncSocketUseCFStreamForTLS"; +#endif +NSString *const GCDAsyncSocketSSLPeerID = @"GCDAsyncSocketSSLPeerID"; +NSString *const GCDAsyncSocketSSLProtocolVersionMin = @"GCDAsyncSocketSSLProtocolVersionMin"; +NSString *const GCDAsyncSocketSSLProtocolVersionMax = @"GCDAsyncSocketSSLProtocolVersionMax"; +NSString *const GCDAsyncSocketSSLSessionOptionFalseStart = @"GCDAsyncSocketSSLSessionOptionFalseStart"; +NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord = @"GCDAsyncSocketSSLSessionOptionSendOneByteRecord"; +NSString *const GCDAsyncSocketSSLCipherSuites = @"GCDAsyncSocketSSLCipherSuites"; +#if !TARGET_OS_IPHONE +NSString *const GCDAsyncSocketSSLDiffieHellmanParameters = @"GCDAsyncSocketSSLDiffieHellmanParameters"; +#endif + +enum GCDAsyncSocketFlags +{ + kSocketStarted = 1 << 0, // If set, socket has been started (accepting/connecting) + kConnected = 1 << 1, // If set, the socket is connected + kForbidReadsWrites = 1 << 2, // If set, no new reads or writes are allowed + kReadsPaused = 1 << 3, // If set, reads are paused due to possible timeout + kWritesPaused = 1 << 4, // If set, writes are paused due to possible timeout + kDisconnectAfterReads = 1 << 5, // If set, disconnect after no more reads are queued + kDisconnectAfterWrites = 1 << 6, // If set, disconnect after no more writes are queued + kSocketCanAcceptBytes = 1 << 7, // If set, we know socket can accept bytes. If unset, it's unknown. + kReadSourceSuspended = 1 << 8, // If set, the read source is suspended + kWriteSourceSuspended = 1 << 9, // If set, the write source is suspended + kQueuedTLS = 1 << 10, // If set, we've queued an upgrade to TLS + kStartingReadTLS = 1 << 11, // If set, we're waiting for TLS negotiation to complete + kStartingWriteTLS = 1 << 12, // If set, we're waiting for TLS negotiation to complete + kSocketSecure = 1 << 13, // If set, socket is using secure communication via SSL/TLS + kSocketHasReadEOF = 1 << 14, // If set, we have read EOF from socket + kReadStreamClosed = 1 << 15, // If set, we've read EOF plus prebuffer has been drained + kDealloc = 1 << 16, // If set, the socket is being deallocated +#if TARGET_OS_IPHONE + kAddedStreamsToRunLoop = 1 << 17, // If set, CFStreams have been added to listener thread + kUsingCFStreamForTLS = 1 << 18, // If set, we're forced to use CFStream instead of SecureTransport + kSecureSocketHasBytesAvailable = 1 << 19, // If set, CFReadStream has notified us of bytes available +#endif +}; + +enum GCDAsyncSocketConfig +{ + kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled + kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled + kPreferIPv6 = 1 << 2, // If set, IPv6 is preferred over IPv4 + kAllowHalfDuplexConnection = 1 << 3, // If set, the socket will stay open even if the read stream closes +}; + +#if TARGET_OS_IPHONE + static NSThread *cfstreamThread; // Used for CFStreams + + + static uint64_t cfstreamThreadRetainCount; // setup & teardown + static dispatch_queue_t cfstreamThreadSetupQueue; // setup & teardown +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * A PreBuffer is used when there is more data available on the socket + * than is being requested by current read request. + * In this case we slurp up all data from the socket (to minimize sys calls), + * and store additional yet unread data in a "prebuffer". + * + * The prebuffer is entirely drained before we read from the socket again. + * In other words, a large chunk of data is written is written to the prebuffer. + * The prebuffer is then drained via a series of one or more reads (for subsequent read request(s)). + * + * A ring buffer was once used for this purpose. + * But a ring buffer takes up twice as much memory as needed (double the size for mirroring). + * In fact, it generally takes up more than twice the needed size as everything has to be rounded up to vm_page_size. + * And since the prebuffer is always completely drained after being written to, a full ring buffer isn't needed. + * + * The current design is very simple and straight-forward, while also keeping memory requirements lower. +**/ + +@interface GCDAsyncSocketPreBuffer : NSObject +{ + uint8_t *preBuffer; + size_t preBufferSize; + + uint8_t *readPointer; + uint8_t *writePointer; +} + +- (id)initWithCapacity:(size_t)numBytes; + +- (void)ensureCapacityForWrite:(size_t)numBytes; + +- (size_t)availableBytes; +- (uint8_t *)readBuffer; + +- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr; + +- (size_t)availableSpace; +- (uint8_t *)writeBuffer; + +- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr; + +- (void)didRead:(size_t)bytesRead; +- (void)didWrite:(size_t)bytesWritten; + +- (void)reset; + +@end + +@implementation GCDAsyncSocketPreBuffer + +- (id)initWithCapacity:(size_t)numBytes +{ + if ((self = [super init])) + { + preBufferSize = numBytes; + preBuffer = malloc(preBufferSize); + + readPointer = preBuffer; + writePointer = preBuffer; + } + return self; +} + +- (void)dealloc +{ + if (preBuffer) + free(preBuffer); +} + +- (void)ensureCapacityForWrite:(size_t)numBytes +{ + size_t availableSpace = [self availableSpace]; + + if (numBytes > availableSpace) + { + size_t additionalBytes = numBytes - availableSpace; + + size_t newPreBufferSize = preBufferSize + additionalBytes; + uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize); + + size_t readPointerOffset = readPointer - preBuffer; + size_t writePointerOffset = writePointer - preBuffer; + + preBuffer = newPreBuffer; + preBufferSize = newPreBufferSize; + + readPointer = preBuffer + readPointerOffset; + writePointer = preBuffer + writePointerOffset; + } +} + +- (size_t)availableBytes +{ + return writePointer - readPointer; +} + +- (uint8_t *)readBuffer +{ + return readPointer; +} + +- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr +{ + if (bufferPtr) *bufferPtr = readPointer; + if (availableBytesPtr) *availableBytesPtr = [self availableBytes]; +} + +- (void)didRead:(size_t)bytesRead +{ + readPointer += bytesRead; + + if (readPointer == writePointer) + { + // The prebuffer has been drained. Reset pointers. + readPointer = preBuffer; + writePointer = preBuffer; + } +} + +- (size_t)availableSpace +{ + return preBufferSize - (writePointer - preBuffer); +} + +- (uint8_t *)writeBuffer +{ + return writePointer; +} + +- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr +{ + if (bufferPtr) *bufferPtr = writePointer; + if (availableSpacePtr) *availableSpacePtr = [self availableSpace]; +} + +- (void)didWrite:(size_t)bytesWritten +{ + writePointer += bytesWritten; +} + +- (void)reset +{ + readPointer = preBuffer; + writePointer = preBuffer; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncReadPacket encompasses the instructions for any given read. + * The content of a read packet allows the code to determine if we're: + * - reading to a certain length + * - reading to a certain separator + * - or simply reading the first chunk of available data +**/ +@interface GCDAsyncReadPacket : NSObject +{ + @public + NSMutableData *buffer; + NSUInteger startOffset; + NSUInteger bytesDone; + NSUInteger maxLength; + NSTimeInterval timeout; + NSUInteger readLength; + NSData *term; + BOOL bufferOwner; + NSUInteger originalBufferLength; + long tag; +} +- (id)initWithData:(NSMutableData *)d + startOffset:(NSUInteger)s + maxLength:(NSUInteger)m + timeout:(NSTimeInterval)t + readLength:(NSUInteger)l + terminator:(NSData *)e + tag:(long)i; + +- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead; + +- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr; + +- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable; +- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr; +- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr; + +- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes; + +@end + +@implementation GCDAsyncReadPacket + +- (id)initWithData:(NSMutableData *)d + startOffset:(NSUInteger)s + maxLength:(NSUInteger)m + timeout:(NSTimeInterval)t + readLength:(NSUInteger)l + terminator:(NSData *)e + tag:(long)i +{ + if((self = [super init])) + { + bytesDone = 0; + maxLength = m; + timeout = t; + readLength = l; + term = [e copy]; + tag = i; + + if (d) + { + buffer = d; + startOffset = s; + bufferOwner = NO; + originalBufferLength = [d length]; + } + else + { + if (readLength > 0) + buffer = [[NSMutableData alloc] initWithLength:readLength]; + else + buffer = [[NSMutableData alloc] initWithLength:0]; + + startOffset = 0; + bufferOwner = YES; + originalBufferLength = 0; + } + } + return self; +} + +/** + * Increases the length of the buffer (if needed) to ensure a read of the given size will fit. +**/ +- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead +{ + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + NSUInteger buffSpace = buffSize - buffUsed; + + if (bytesToRead > buffSpace) + { + NSUInteger buffInc = bytesToRead - buffSpace; + + [buffer increaseLengthBy:buffInc]; + } +} + +/** + * This method is used when we do NOT know how much data is available to be read from the socket. + * This method returns the default value unless it exceeds the specified readLength or maxLength. + * + * Furthermore, the shouldPreBuffer decision is based upon the packet type, + * and whether the returned value would fit in the current buffer without requiring a resize of the buffer. +**/ +- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr +{ + NSUInteger result; + + if (readLength > 0) + { + // Read a specific length of data + + result = MIN(defaultValue, (readLength - bytesDone)); + + // There is no need to prebuffer since we know exactly how much data we need to read. + // Even if the buffer isn't currently big enough to fit this amount of data, + // it would have to be resized eventually anyway. + + if (shouldPreBufferPtr) + *shouldPreBufferPtr = NO; + } + else + { + // Either reading until we find a specified terminator, + // or we're simply reading all available data. + // + // In other words, one of: + // + // - readDataToData packet + // - readDataWithTimeout packet + + if (maxLength > 0) + result = MIN(defaultValue, (maxLength - bytesDone)); + else + result = defaultValue; + + // Since we don't know the size of the read in advance, + // the shouldPreBuffer decision is based upon whether the returned value would fit + // in the current buffer without requiring a resize of the buffer. + // + // This is because, in all likelyhood, the amount read from the socket will be less than the default value. + // Thus we should avoid over-allocating the read buffer when we can simply use the pre-buffer instead. + + if (shouldPreBufferPtr) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + NSUInteger buffSpace = buffSize - buffUsed; + + if (buffSpace >= result) + *shouldPreBufferPtr = NO; + else + *shouldPreBufferPtr = YES; + } + } + + return result; +} + +/** + * For read packets without a set terminator, returns the amount of data + * that can be read without exceeding the readLength or maxLength. + * + * The given parameter indicates the number of bytes estimated to be available on the socket, + * which is taken into consideration during the calculation. + * + * The given hint MUST be greater than zero. +**/ +- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable +{ + NSAssert(term == nil, @"This method does not apply to term reads"); + NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); + + if (readLength > 0) + { + // Read a specific length of data + + return MIN(bytesAvailable, (readLength - bytesDone)); + + // No need to avoid resizing the buffer. + // If the user provided their own buffer, + // and told us to read a certain length of data that exceeds the size of the buffer, + // then it is clear that our code will resize the buffer during the read operation. + // + // This method does not actually do any resizing. + // The resizing will happen elsewhere if needed. + } + else + { + // Read all available data + + NSUInteger result = bytesAvailable; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + // No need to avoid resizing the buffer. + // If the user provided their own buffer, + // and told us to read all available data without giving us a maxLength, + // then it is clear that our code might resize the buffer during the read operation. + // + // This method does not actually do any resizing. + // The resizing will happen elsewhere if needed. + + return result; + } +} + +/** + * For read packets with a set terminator, returns the amount of data + * that can be read without exceeding the maxLength. + * + * The given parameter indicates the number of bytes estimated to be available on the socket, + * which is taken into consideration during the calculation. + * + * To optimize memory allocations, mem copies, and mem moves + * the shouldPreBuffer boolean value will indicate if the data should be read into a prebuffer first, + * or if the data can be read directly into the read packet's buffer. +**/ +- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); + + + NSUInteger result = bytesAvailable; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + // Should the data be read into the read packet's buffer, or into a pre-buffer first? + // + // One would imagine the preferred option is the faster one. + // So which one is faster? + // + // Reading directly into the packet's buffer requires: + // 1. Possibly resizing packet buffer (malloc/realloc) + // 2. Filling buffer (read) + // 3. Searching for term (memcmp) + // 4. Possibly copying overflow into prebuffer (malloc/realloc, memcpy) + // + // Reading into prebuffer first: + // 1. Possibly resizing prebuffer (malloc/realloc) + // 2. Filling buffer (read) + // 3. Searching for term (memcmp) + // 4. Copying underflow into packet buffer (malloc/realloc, memcpy) + // 5. Removing underflow from prebuffer (memmove) + // + // Comparing the performance of the two we can see that reading + // data into the prebuffer first is slower due to the extra memove. + // + // However: + // The implementation of NSMutableData is open source via core foundation's CFMutableData. + // Decreasing the length of a mutable data object doesn't cause a realloc. + // In other words, the capacity of a mutable data object can grow, but doesn't shrink. + // + // This means the prebuffer will rarely need a realloc. + // The packet buffer, on the other hand, may often need a realloc. + // This is especially true if we are the buffer owner. + // Furthermore, if we are constantly realloc'ing the packet buffer, + // and then moving the overflow into the prebuffer, + // then we're consistently over-allocating memory for each term read. + // And now we get into a bit of a tradeoff between speed and memory utilization. + // + // The end result is that the two perform very similarly. + // And we can answer the original question very simply by another means. + // + // If we can read all the data directly into the packet's buffer without resizing it first, + // then we do so. Otherwise we use the prebuffer. + + if (shouldPreBufferPtr) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + if ((buffSize - buffUsed) >= result) + *shouldPreBufferPtr = NO; + else + *shouldPreBufferPtr = YES; + } + + return result; +} + +/** + * For read packets with a set terminator, + * returns the amount of data that can be read from the given preBuffer, + * without going over a terminator or the maxLength. + * + * It is assumed the terminator has not already been read. +**/ +- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert([preBuffer availableBytes] > 0, @"Invoked with empty pre buffer!"); + + // We know that the terminator, as a whole, doesn't exist in our own buffer. + // But it is possible that a _portion_ of it exists in our buffer. + // So we're going to look for the terminator starting with a portion of our own buffer. + // + // Example: + // + // term length = 3 bytes + // bytesDone = 5 bytes + // preBuffer length = 5 bytes + // + // If we append the preBuffer to our buffer, + // it would look like this: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // --------------------- + // + // So we start our search here: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // -------^-^-^--------- + // + // And move forwards... + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------^-^-^------- + // + // Until we find the terminator or reach the end. + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------------^-^-^- + + BOOL found = NO; + + NSUInteger termLength = [term length]; + NSUInteger preBufferLength = [preBuffer availableBytes]; + + if ((bytesDone + preBufferLength) < termLength) + { + // Not enough data for a full term sequence yet + return preBufferLength; + } + + NSUInteger maxPreBufferLength; + if (maxLength > 0) { + maxPreBufferLength = MIN(preBufferLength, (maxLength - bytesDone)); + + // Note: maxLength >= termLength + } + else { + maxPreBufferLength = preBufferLength; + } + + uint8_t seq[termLength]; + const void *termBuf = [term bytes]; + + NSUInteger bufLen = MIN(bytesDone, (termLength - 1)); + uint8_t *buf = (uint8_t *)[buffer mutableBytes] + startOffset + bytesDone - bufLen; + + NSUInteger preLen = termLength - bufLen; + const uint8_t *pre = [preBuffer readBuffer]; + + NSUInteger loopCount = bufLen + maxPreBufferLength - termLength + 1; // Plus one. See example above. + + NSUInteger result = maxPreBufferLength; + + NSUInteger i; + for (i = 0; i < loopCount; i++) + { + if (bufLen > 0) + { + // Combining bytes from buffer and preBuffer + + memcpy(seq, buf, bufLen); + memcpy(seq + bufLen, pre, preLen); + + if (memcmp(seq, termBuf, termLength) == 0) + { + result = preLen; + found = YES; + break; + } + + buf++; + bufLen--; + preLen++; + } + else + { + // Comparing directly from preBuffer + + if (memcmp(pre, termBuf, termLength) == 0) + { + NSUInteger preOffset = pre - [preBuffer readBuffer]; // pointer arithmetic + + result = preOffset + termLength; + found = YES; + break; + } + + pre++; + } + } + + // There is no need to avoid resizing the buffer in this particular situation. + + if (foundPtr) *foundPtr = found; + return result; +} + +/** + * For read packets with a set terminator, scans the packet buffer for the term. + * It is assumed the terminator had not been fully read prior to the new bytes. + * + * If the term is found, the number of excess bytes after the term are returned. + * If the term is not found, this method will return -1. + * + * Note: A return value of zero means the term was found at the very end. + * + * Prerequisites: + * The given number of bytes have been added to the end of our buffer. + * Our bytesDone variable has NOT been changed due to the prebuffered bytes. +**/ +- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + + // The implementation of this method is very similar to the above method. + // See the above method for a discussion of the algorithm used here. + + uint8_t *buff = [buffer mutableBytes]; + NSUInteger buffLength = bytesDone + numBytes; + + const void *termBuff = [term bytes]; + NSUInteger termLength = [term length]; + + // Note: We are dealing with unsigned integers, + // so make sure the math doesn't go below zero. + + NSUInteger i = ((buffLength - numBytes) >= termLength) ? (buffLength - numBytes - termLength + 1) : 0; + + while (i + termLength <= buffLength) + { + uint8_t *subBuffer = buff + startOffset + i; + + if (memcmp(subBuffer, termBuff, termLength) == 0) + { + return buffLength - (i + termLength); + } + + i++; + } + + return -1; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncWritePacket encompasses the instructions for any given write. +**/ +@interface GCDAsyncWritePacket : NSObject +{ + @public + NSData *buffer; + NSUInteger bytesDone; + long tag; + NSTimeInterval timeout; +} +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i; +@end + +@implementation GCDAsyncWritePacket + +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i +{ + if((self = [super init])) + { + buffer = d; // Retain not copy. For performance as documented in header file. + bytesDone = 0; + timeout = t; + tag = i; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncSpecialPacket encompasses special instructions for interruptions in the read/write queues. + * This class my be altered to support more than just TLS in the future. +**/ +@interface GCDAsyncSpecialPacket : NSObject +{ + @public + NSDictionary *tlsSettings; +} +- (id)initWithTLSSettings:(NSDictionary *)settings; +@end + +@implementation GCDAsyncSpecialPacket + +- (id)initWithTLSSettings:(NSDictionary *)settings +{ + if((self = [super init])) + { + tlsSettings = [settings copy]; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation GCDAsyncSocket +{ + uint32_t flags; + uint16_t config; + + __weak id delegate; + dispatch_queue_t delegateQueue; + + int socket4FD; + int socket6FD; + int socketUN; + NSURL *socketUrl; + int stateIndex; + NSData * connectInterface4; + NSData * connectInterface6; + NSData * connectInterfaceUN; + + dispatch_queue_t socketQueue; + + dispatch_source_t accept4Source; + dispatch_source_t accept6Source; + dispatch_source_t acceptUNSource; + dispatch_source_t connectTimer; + dispatch_source_t readSource; + dispatch_source_t writeSource; + dispatch_source_t readTimer; + dispatch_source_t writeTimer; + + NSMutableArray *readQueue; + NSMutableArray *writeQueue; + + GCDAsyncReadPacket *currentRead; + GCDAsyncWritePacket *currentWrite; + + unsigned long socketFDBytesAvailable; + + GCDAsyncSocketPreBuffer *preBuffer; + +#if TARGET_OS_IPHONE + CFStreamClientContext streamContext; + CFReadStreamRef readStream; + CFWriteStreamRef writeStream; +#endif + SSLContextRef sslContext; + GCDAsyncSocketPreBuffer *sslPreBuffer; + size_t sslWriteCachedLength; + OSStatus sslErrCode; + OSStatus lastSSLHandshakeError; + + void *IsOnSocketQueueOrTargetQueueKey; + + id userData; + NSTimeInterval alternateAddressDelay; +} + +- (id)init +{ + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; +} + +- (id)initWithSocketQueue:(dispatch_queue_t)sq +{ + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; +} + +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq +{ + return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; +} + +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq +{ + if((self = [super init])) + { + delegate = aDelegate; + delegateQueue = dq; + + #if !OS_OBJECT_USE_OBJC + if (dq) dispatch_retain(dq); + #endif + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + socketUN = SOCKET_NULL; + socketUrl = nil; + stateIndex = 0; + + if (sq) + { + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + + socketQueue = sq; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(sq); + #endif + } + else + { + socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL); + } + + // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. + // From the documentation: + // + // > Keys are only compared as pointers and are never dereferenced. + // > Thus, you can use a pointer to a static variable for a specific subsystem or + // > any other value that allows you to identify the value uniquely. + // + // We're just going to use the memory address of an ivar. + // Specifically an ivar that is explicitly named for our purpose to make the code more readable. + // + // However, it feels tedious (and less readable) to include the "&" all the time: + // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) + // + // So we're going to make it so it doesn't matter if we use the '&' or not, + // by assigning the value of the ivar to the address of the ivar. + // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; + + IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; + + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); + + readQueue = [[NSMutableArray alloc] initWithCapacity:5]; + currentRead = nil; + + writeQueue = [[NSMutableArray alloc] initWithCapacity:5]; + currentWrite = nil; + + preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; + alternateAddressDelay = 0.3; + } + return self; +} + +- (void)dealloc +{ + LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); + + // Set dealloc flag. + // This is used by closeWithError to ensure we don't accidentally retain ourself. + flags |= kDealloc; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + [self closeWithError:nil]; + } + else + { + dispatch_sync(socketQueue, ^{ + [self closeWithError:nil]; + }); + } + + delegate = nil; + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + #endif + delegateQueue = NULL; + + #if !OS_OBJECT_USE_OBJC + if (socketQueue) dispatch_release(socketQueue); + #endif + socketQueue = NULL; + + LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)delegate +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegate; + } + else + { + __block id result; + + dispatch_sync(socketQueue, ^{ + result = delegate; + }); + + return result; + } +} + +- (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + delegate = newDelegate; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id)newDelegate +{ + [self setDelegate:newDelegate synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id)newDelegate +{ + [self setDelegate:newDelegate synchronously:YES]; +} + +- (dispatch_queue_t)delegateQueue +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegateQueue; + } + else + { + __block dispatch_queue_t result; + + dispatch_sync(socketQueue, ^{ + result = delegateQueue; + }); + + return result; + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); + #endif + + delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:YES]; +} + +- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (delegatePtr) *delegatePtr = delegate; + if (delegateQueuePtr) *delegateQueuePtr = delegateQueue; + } + else + { + __block id dPtr = NULL; + __block dispatch_queue_t dqPtr = NULL; + + dispatch_sync(socketQueue, ^{ + dPtr = delegate; + dqPtr = delegateQueue; + }); + + if (delegatePtr) *delegatePtr = dPtr; + if (delegateQueuePtr) *delegateQueuePtr = dqPtr; + } +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + delegate = newDelegate; + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); + #endif + + delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES]; +} + +- (BOOL)isIPv4Enabled +{ + // Note: YES means kIPv4Disabled is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kIPv4Disabled) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kIPv4Disabled) == 0); + }); + + return result; + } +} + +- (void)setIPv4Enabled:(BOOL)flag +{ + // Note: YES means kIPv4Disabled is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kIPv4Disabled; + else + config |= kIPv4Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv6Enabled +{ + // Note: YES means kIPv6Disabled is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kIPv6Disabled) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kIPv6Disabled) == 0); + }); + + return result; + } +} + +- (void)setIPv6Enabled:(BOOL)flag +{ + // Note: YES means kIPv6Disabled is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kIPv6Disabled; + else + config |= kIPv6Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv4PreferredOverIPv6 +{ + // Note: YES means kPreferIPv6 is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kPreferIPv6) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kPreferIPv6) == 0); + }); + + return result; + } +} + +- (void)setIPv4PreferredOverIPv6:(BOOL)flag +{ + // Note: YES means kPreferIPv6 is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kPreferIPv6; + else + config |= kPreferIPv6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (NSTimeInterval) alternateAddressDelay { + __block NSTimeInterval delay; + dispatch_block_t block = ^{ + delay = alternateAddressDelay; + }; + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + return delay; +} + +- (void) setAlternateAddressDelay:(NSTimeInterval)delay { + dispatch_block_t block = ^{ + alternateAddressDelay = delay; + }; + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (id)userData +{ + __block id result = nil; + + dispatch_block_t block = ^{ + + result = userData; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setUserData:(id)arbitraryUserData +{ + dispatch_block_t block = ^{ + + if (userData != arbitraryUserData) + { + userData = arbitraryUserData; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Accepting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr +{ + return [self acceptOnInterface:nil port:port error:errPtr]; +} + +- (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr +{ + LogTrace(); + + // Just in-case interface parameter is immutable. + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *err = nil; + + // CreateSocket Block + // This block will be invoked within the dispatch block below. + + int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { + + int socketFD = socket(domain, SOCK_STREAM, 0); + + if (socketFD == SOCKET_NULL) + { + NSString *reason = @"Error in socket() function"; + err = [self errnoErrorWithReason:reason]; + + return SOCKET_NULL; + } + + int status; + + // Set socket options + + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (status == -1) + { + NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + int reuseOn = 1; + status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + if (status == -1) + { + NSString *reason = @"Error enabling address reuse (setsockopt)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Bind socket + + status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); + if (status == -1) + { + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Listen + + status = listen(socketFD, 1024); + if (status == -1) + { + NSString *reason = @"Error in listen() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + return socketFD; + }; + + // Create dispatch block and run on socketQueue + + dispatch_block_t block = ^{ @autoreleasepool { + + if (delegate == nil) // Must have delegate set + { + NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (![self isDisconnected]) // Must be disconnected + { + NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + // Resolve interface from description + + NSMutableData *interface4 = nil; + NSMutableData *interface6 = nil; + + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port]; + + if ((interface4 == nil) && (interface6 == nil)) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv4Disabled && (interface6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL enableIPv4 = !isIPv4Disabled && (interface4 != nil); + BOOL enableIPv6 = !isIPv6Disabled && (interface6 != nil); + + // Create sockets, configure, bind, and listen + + if (enableIPv4) + { + LogVerbose(@"Creating IPv4 socket"); + socket4FD = createSocket(AF_INET, interface4); + + if (socket4FD == SOCKET_NULL) + { + return_from_block; + } + } + + if (enableIPv6) + { + LogVerbose(@"Creating IPv6 socket"); + + if (enableIPv4 && (port == 0)) + { + // No specific port was specified, so we allowed the OS to pick an available port for us. + // Now we need to make sure the IPv6 socket listens on the same port as the IPv4 socket. + + struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)[interface6 mutableBytes]; + addr6->sin6_port = htons([self localPort4]); + } + + socket6FD = createSocket(AF_INET6, interface6); + + if (socket6FD == SOCKET_NULL) + { + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"close(socket4FD)"); + close(socket4FD); + } + + return_from_block; + } + } + + // Create accept sources + + if (enableIPv4) + { + accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue); + + int socketFD = socket4FD; + dispatch_source_t acceptSource = accept4Source; + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(accept4Source, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"event4Block"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); + + #pragma clang diagnostic pop + }}); + + + dispatch_source_set_cancel_handler(accept4Source, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(accept4Source)"); + dispatch_release(acceptSource); + #endif + + LogVerbose(@"close(socket4FD)"); + close(socketFD); + + #pragma clang diagnostic pop + }); + + LogVerbose(@"dispatch_resume(accept4Source)"); + dispatch_resume(accept4Source); + } + + if (enableIPv6) + { + accept6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket6FD, 0, socketQueue); + + int socketFD = socket6FD; + dispatch_source_t acceptSource = accept6Source; + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(accept6Source, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"event6Block"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); + + #pragma clang diagnostic pop + }}); + + dispatch_source_set_cancel_handler(accept6Source, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(accept6Source)"); + dispatch_release(acceptSource); + #endif + + LogVerbose(@"close(socket6FD)"); + close(socketFD); + + #pragma clang diagnostic pop + }); + + LogVerbose(@"dispatch_resume(accept6Source)"); + dispatch_resume(accept6Source); + } + + flags |= kSocketStarted; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + LogInfo(@"Error in accept: %@", err); + + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr; +{ + LogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + // CreateSocket Block + // This block will be invoked within the dispatch block below. + + int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { + + int socketFD = socket(domain, SOCK_STREAM, 0); + + if (socketFD == SOCKET_NULL) + { + NSString *reason = @"Error in socket() function"; + err = [self errnoErrorWithReason:reason]; + + return SOCKET_NULL; + } + + int status; + + // Set socket options + + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (status == -1) + { + NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + int reuseOn = 1; + status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + if (status == -1) + { + NSString *reason = @"Error enabling address reuse (setsockopt)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Bind socket + + status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); + if (status == -1) + { + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Listen + + status = listen(socketFD, 1024); + if (status == -1) + { + NSString *reason = @"Error in listen() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + return socketFD; + }; + + // Create dispatch block and run on socketQueue + + dispatch_block_t block = ^{ @autoreleasepool { + + if (delegate == nil) // Must have delegate set + { + NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (![self isDisconnected]) // Must be disconnected + { + NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + // Remove a previous socket + + NSError *error = nil; + NSFileManager *fileManager = [NSFileManager defaultManager]; + if ([fileManager fileExistsAtPath:url.path]) { + if (![[NSFileManager defaultManager] removeItemAtURL:url error:&error]) { + NSString *msg = @"Could not remove previous unix domain socket at given url."; + err = [self otherError:msg]; + + return_from_block; + } + } + + // Resolve interface from description + + NSData *interface = [self getInterfaceAddressFromUrl:url]; + + if (interface == nil) + { + NSString *msg = @"Invalid unix domain url. Specify a valid file url that does not exist (e.g. \"file:///tmp/socket\")"; + err = [self badParamError:msg]; + + return_from_block; + } + + // Create sockets, configure, bind, and listen + + LogVerbose(@"Creating unix domain socket"); + socketUN = createSocket(AF_UNIX, interface); + + if (socketUN == SOCKET_NULL) + { + return_from_block; + } + + socketUrl = url; + + // Create accept sources + + acceptUNSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketUN, 0, socketQueue); + + int socketFD = socketUN; + dispatch_source_t acceptSource = acceptUNSource; + + dispatch_source_set_event_handler(acceptUNSource, ^{ @autoreleasepool { + + LogVerbose(@"eventUNBlock"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([self doAccept:socketFD] && (++i < numPendingConnections)); + }}); + + dispatch_source_set_cancel_handler(acceptUNSource, ^{ + +#if NEEDS_DISPATCH_RETAIN_RELEASE + LogVerbose(@"dispatch_release(accept4Source)"); + dispatch_release(acceptSource); +#endif + + LogVerbose(@"close(socket4FD)"); + close(socketFD); + }); + + LogVerbose(@"dispatch_resume(accept4Source)"); + dispatch_resume(acceptUNSource); + + flags |= kSocketStarted; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + LogInfo(@"Error in accept: %@", err); + + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)doAccept:(int)parentSocketFD +{ + LogTrace(); + + int socketType; + int childSocketFD; + NSData *childSocketAddress; + + if (parentSocketFD == socket4FD) + { + socketType = 0; + + struct sockaddr_in addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + else if (parentSocketFD == socket6FD) + { + socketType = 1; + + struct sockaddr_in6 addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + else // if (parentSocketFD == socketUN) + { + socketType = 2; + + struct sockaddr_un addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + + // Enable non-blocking IO on the socket + + int result = fcntl(childSocketFD, F_SETFL, O_NONBLOCK); + if (result == -1) + { + LogWarn(@"Error enabling non-blocking IO on accepted socket (fcntl)"); + return NO; + } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(childSocketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Notify delegate + + if (delegateQueue) + { + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + // Query delegate for custom socket queue + + dispatch_queue_t childSocketQueue = NULL; + + if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)]) + { + childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress + onSocket:self]; + } + + // Create GCDAsyncSocket instance for accepted socket + + GCDAsyncSocket *acceptedSocket = [[[self class] alloc] initWithDelegate:theDelegate + delegateQueue:delegateQueue + socketQueue:childSocketQueue]; + + if (socketType == 0) + acceptedSocket->socket4FD = childSocketFD; + else if (socketType == 1) + acceptedSocket->socket6FD = childSocketFD; + else + acceptedSocket->socketUN = childSocketFD; + + acceptedSocket->flags = (kSocketStarted | kConnected); + + // Setup read and write sources for accepted socket + + dispatch_async(acceptedSocket->socketQueue, ^{ @autoreleasepool { + + [acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD]; + }}); + + // Notify delegate + + if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]) + { + [theDelegate socket:self didAcceptNewSocket:acceptedSocket]; + } + + // Release the socket queue returned from the delegate (it was retained by acceptedSocket) + #if !OS_OBJECT_USE_OBJC + if (childSocketQueue) dispatch_release(childSocketQueue); + #endif + + // The accepted socket should have been retained by the delegate. + // Otherwise it gets properly released when exiting the block. + }}); + } + + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method runs through the various checks required prior to a connection attempt. + * It is shared between the connectToHost and connectToAddress methods. + * +**/ +- (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (delegate == nil) // Must have delegate set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (![self isDisconnected]) // Must be disconnected + { + if (errPtr) + { + NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + if (errPtr) + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (interface) + { + NSMutableData *interface4 = nil; + NSMutableData *interface6 = nil; + + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0]; + + if ((interface4 == nil) && (interface6 == nil)) + { + if (errPtr) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + if (isIPv4Disabled && (interface6 == nil)) + { + if (errPtr) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + if (errPtr) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + connectInterface4 = interface4; + connectInterface6 = interface6; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + return YES; +} + +- (BOOL)preConnectWithUrl:(NSURL *)url error:(NSError **)errPtr +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (delegate == nil) // Must have delegate set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (![self isDisconnected]) // Must be disconnected + { + if (errPtr) + { + NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + NSData *interface = [self getInterfaceAddressFromUrl:url]; + + if (interface == nil) + { + if (errPtr) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + connectInterfaceUN = interface; + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + return YES; +} + +- (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr +{ + return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr]; +} + +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr]; +} + +- (BOOL)connectToHost:(NSString *)inHost + onPort:(uint16_t)port + viaInterface:(NSString *)inInterface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + LogTrace(); + + // Just in case immutable objects were passed + NSString *host = [inHost copy]; + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *preConnectErr = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with host parameter + + if ([host length] == 0) + { + NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; + preConnectErr = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithInterface:interface error:&preConnectErr]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + flags |= kSocketStarted; + + LogVerbose(@"Dispatching DNS lookup..."); + + // It's possible that the given host parameter is actually a NSMutableString. + // So we want to copy it now, within this block that will be executed synchronously. + // This way the asynchronous lookup block below doesn't have to worry about it changing. + + NSString *hostCpy = [host copy]; + + int aStateIndex = stateIndex; + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + NSError *lookupErr = nil; + NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr]; + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + if (lookupErr) + { + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf lookup:aStateIndex didFail:lookupErr]; + }}); + } + else + { + NSData *address4 = nil; + NSData *address6 = nil; + + for (NSData *address in addresses) + { + if (!address4 && [[self class] isIPv4Address:address]) + { + address4 = address; + } + else if (!address6 && [[self class] isIPv6Address:address]) + { + address6 = address; + } + } + + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; + }}); + } + + #pragma clang diagnostic pop + }}); + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + + if (errPtr) *errPtr = preConnectErr; + return result; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr]; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:timeout error:errPtr]; +} + +- (BOOL)connectToAddress:(NSData *)inRemoteAddr + viaInterface:(NSString *)inInterface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + LogTrace(); + + // Just in case immutable objects were passed + NSData *remoteAddr = [inRemoteAddr copy]; + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with remoteAddr parameter + + NSData *address4 = nil; + NSData *address6 = nil; + + if ([remoteAddr length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddr = (const struct sockaddr *)[remoteAddr bytes]; + + if (sockaddr->sa_family == AF_INET) + { + if ([remoteAddr length] == sizeof(struct sockaddr_in)) + { + address4 = remoteAddr; + } + } + else if (sockaddr->sa_family == AF_INET6) + { + if ([remoteAddr length] == sizeof(struct sockaddr_in6)) + { + address6 = remoteAddr; + } + } + } + + if ((address4 == nil) && (address6 == nil)) + { + NSString *msg = @"A valid IPv4 or IPv6 address was not given"; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (address4 != nil)) + { + NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (address6 != nil)) + { + NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithInterface:interface error:&err]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + if (![self connectWithAddress4:address4 address6:address6 error:&err]) + { + return_from_block; + } + + flags |= kSocketStarted; + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; +{ + LogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with host parameter + + if ([url.path length] == 0) + { + NSString *msg = @"Invalid unix domain socket url."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithUrl:url error:&err]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + flags |= kSocketStarted; + + // Start the normal connection process + + NSError *connectError = nil; + if (![self connectWithAddressUN:connectInterfaceUN error:&connectError]) + { + [self closeWithError:connectError]; + + return_from_block; + } + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6 +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(address4 || address6, @"Expected at least one valid address"); + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring lookupDidSucceed, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + // Check for problems + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (address6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + if (isIPv6Disabled && (address4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + // Start the normal connection process + + NSError *err = nil; + if (![self connectWithAddress4:address4 address6:address6 error:&err]) + { + [self closeWithError:err]; + } +} + +/** + * This method is called if the DNS lookup fails. + * This method is executed on the socketQueue. + * + * Since the DNS lookup executed synchronously on a global concurrent queue, + * the original connection request may have already been cancelled or timed-out by the time this method is invoked. + * The lookupIndex tells us whether the lookup is still valid or not. +**/ +- (void)lookup:(int)aStateIndex didFail:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring lookup:didFail: - already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + [self endConnectTimeout]; + [self closeWithError:error]; +} + +- (BOOL)bindSocket:(int)socketFD toInterface:(NSData *)connectInterface error:(NSError **)errPtr +{ + // Bind the socket to the desired interface (if needed) + + if (connectInterface) + { + LogVerbose(@"Binding socket..."); + + if ([[self class] portFromAddress:connectInterface] > 0) + { + // Since we're going to be binding to a specific port, + // we should turn on reuseaddr to allow us to override sockets in time_wait. + + int reuseOn = 1; + setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + } + + const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes]; + + int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]); + if (result != 0) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; + + return NO; + } + } + + return YES; +} + +- (int)createSocket:(int)family connectInterface:(NSData *)connectInterface errPtr:(NSError **)errPtr +{ + int socketFD = socket(family, SOCK_STREAM, 0); + + if (socketFD == SOCKET_NULL) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; + + return socketFD; + } + + if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr]) + { + [self closeSocket:socketFD]; + + return SOCKET_NULL; + } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + return socketFD; +} + +- (void)connectSocket:(int)socketFD address:(NSData *)address stateIndex:(int)aStateIndex +{ + // If there already is a socket connected, we close socketFD and return + if (self.isConnected) + { + [self closeSocket:socketFD]; + return; + } + + // Start the connection process in a background queue + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]); + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + if (strongSelf.isConnected) + { + [strongSelf closeSocket:socketFD]; + return_from_block; + } + + if (result == 0) + { + [self closeUnusedSocket:socketFD]; + + [strongSelf didConnect:aStateIndex]; + } + else + { + [strongSelf closeSocket:socketFD]; + + // If there are no more sockets trying to connect, we inform the error to the delegate + if (strongSelf.socket4FD == SOCKET_NULL && strongSelf.socket6FD == SOCKET_NULL) + { + NSError *error = [strongSelf errnoErrorWithReason:@"Error in connect() function"]; + [strongSelf didNotConnect:aStateIndex error:error]; + } + } + }}); + +#pragma clang diagnostic pop + }); + + LogVerbose(@"Connecting..."); +} + +- (void)closeSocket:(int)socketFD +{ + if (socketFD != SOCKET_NULL && + (socketFD == socket6FD || socketFD == socket4FD)) + { + close(socketFD); + + if (socketFD == socket4FD) + { + LogVerbose(@"close(socket4FD)"); + socket4FD = SOCKET_NULL; + } + else if (socketFD == socket6FD) + { + LogVerbose(@"close(socket6FD)"); + socket6FD = SOCKET_NULL; + } + } +} + +- (void)closeUnusedSocket:(int)usedSocketFD +{ + if (usedSocketFD != socket4FD) + { + [self closeSocket:socket4FD]; + } + else if (usedSocketFD != socket6FD) + { + [self closeSocket:socket6FD]; + } +} + +- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]); + LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]); + + // Determine socket type + + BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; + + // Create and bind the sockets + + if (address4) + { + LogVerbose(@"Creating IPv4 socket"); + + socket4FD = [self createSocket:AF_INET connectInterface:connectInterface4 errPtr:errPtr]; + } + + if (address6) + { + LogVerbose(@"Creating IPv6 socket"); + + socket6FD = [self createSocket:AF_INET6 connectInterface:connectInterface6 errPtr:errPtr]; + } + + if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL) + { + return NO; + } + + int socketFD, alternateSocketFD; + NSData *address, *alternateAddress; + + if ((preferIPv6 && socket6FD) || socket4FD == SOCKET_NULL) + { + socketFD = socket6FD; + alternateSocketFD = socket4FD; + address = address6; + alternateAddress = address4; + } + else + { + socketFD = socket4FD; + alternateSocketFD = socket6FD; + address = address4; + alternateAddress = address6; + } + + int aStateIndex = stateIndex; + + [self connectSocket:socketFD address:address stateIndex:aStateIndex]; + + if (alternateAddress) + { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(alternateAddressDelay * NSEC_PER_SEC)), socketQueue, ^{ + [self connectSocket:alternateSocketFD address:alternateAddress stateIndex:aStateIndex]; + }); + } + + return YES; +} + +- (BOOL)connectWithAddressUN:(NSData *)address error:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + // Create the socket + + int socketFD; + + LogVerbose(@"Creating unix domain socket"); + + socketUN = socket(AF_UNIX, SOCK_STREAM, 0); + + socketFD = socketUN; + + if (socketFD == SOCKET_NULL) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; + + return NO; + } + + // Bind the socket to the desired interface (if needed) + + LogVerbose(@"Binding socket..."); + + int reuseOn = 1; + setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + +// const struct sockaddr *interfaceAddr = (const struct sockaddr *)[address bytes]; +// +// int result = bind(socketFD, interfaceAddr, (socklen_t)[address length]); +// if (result != 0) +// { +// if (errPtr) +// *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; +// +// return NO; +// } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Start the connection process in a background queue + + int aStateIndex = stateIndex; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ + + const struct sockaddr *addr = (const struct sockaddr *)[address bytes]; + int result = connect(socketFD, addr, addr->sa_len); + if (result == 0) + { + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self didConnect:aStateIndex]; + }}); + } + else + { + // TODO: Bad file descriptor + perror("connect"); + NSError *error = [self errnoErrorWithReason:@"Error in connect() function"]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self didNotConnect:aStateIndex error:error]; + }}); + } + }); + + LogVerbose(@"Connecting..."); + + return YES; +} + +- (void)didConnect:(int)aStateIndex +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring didConnect, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + flags |= kConnected; + + [self endConnectTimeout]; + + #if TARGET_OS_IPHONE + // The endConnectTimeout method executed above incremented the stateIndex. + aStateIndex = stateIndex; + #endif + + // Setup read/write streams (as workaround for specific shortcomings in the iOS platform) + // + // Note: + // There may be configuration options that must be set by the delegate before opening the streams. + // The primary example is the kCFStreamNetworkServiceTypeVoIP flag, which only works on an unopened stream. + // + // Thus we wait until after the socket:didConnectToHost:port: delegate method has completed. + // This gives the delegate time to properly configure the streams if needed. + + dispatch_block_t SetupStreamsPart1 = ^{ + #if TARGET_OS_IPHONE + + if (![self createReadAndWriteStream]) + { + [self closeWithError:[self otherError:@"Error creating CFStreams"]]; + return; + } + + if (![self registerForStreamCallbacksIncludingReadWrite:NO]) + { + [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; + return; + } + + #endif + }; + dispatch_block_t SetupStreamsPart2 = ^{ + #if TARGET_OS_IPHONE + + if (aStateIndex != stateIndex) + { + // The socket has been disconnected. + return; + } + + if (![self addStreamsToRunLoop]) + { + [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; + return; + } + + if (![self openStreams]) + { + [self closeWithError:[self otherError:@"Error creating CFStreams"]]; + return; + } + + #endif + }; + + // Notify delegate + + NSString *host = [self connectedHost]; + uint16_t port = [self connectedPort]; + NSURL *url = [self connectedUrl]; + + __strong id theDelegate = delegate; + + if (delegateQueue && host != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToHost:port:)]) + { + SetupStreamsPart1(); + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didConnectToHost:host port:port]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + SetupStreamsPart2(); + }}); + }}); + } + else if (delegateQueue && url != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToUrl:)]) + { + SetupStreamsPart1(); + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didConnectToUrl:url]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + SetupStreamsPart2(); + }}); + }}); + } + else + { + SetupStreamsPart1(); + SetupStreamsPart2(); + } + + // Get the connected socket + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + // Enable non-blocking IO on the socket + + int result = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (result == -1) + { + NSString *errMsg = @"Error enabling non-blocking IO on socket (fcntl)"; + [self closeWithError:[self otherError:errMsg]]; + + return; + } + + // Setup our read/write sources + + [self setupReadAndWriteSourcesForNewlyConnectedSocket:socketFD]; + + // Dequeue any pending read/write requests + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; +} + +- (void)didNotConnect:(int)aStateIndex error:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring didNotConnect, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + [self closeWithError:error]; +} + +- (void)startConnectTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(connectTimer, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doConnectTimeout]; + + #pragma clang diagnostic pop + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theConnectTimer = connectTimer; + dispatch_source_set_cancel_handler(connectTimer, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(connectTimer)"); + dispatch_release(theConnectTimer); + + #pragma clang diagnostic pop + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + dispatch_source_set_timer(connectTimer, tt, DISPATCH_TIME_FOREVER, 0); + + dispatch_resume(connectTimer); + } +} + +- (void)endConnectTimeout +{ + LogTrace(); + + if (connectTimer) + { + dispatch_source_cancel(connectTimer); + connectTimer = NULL; + } + + // Increment stateIndex. + // This will prevent us from processing results from any related background asynchronous operations. + // + // Note: This should be called from close method even if connectTimer is NULL. + // This is because one might disconnect a socket prior to a successful connection which had no timeout. + + stateIndex++; + + if (connectInterface4) + { + connectInterface4 = nil; + } + if (connectInterface6) + { + connectInterface6 = nil; + } +} + +- (void)doConnectTimeout +{ + LogTrace(); + + [self endConnectTimeout]; + [self closeWithError:[self connectTimeoutError]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Disconnecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)closeWithError:(NSError *)error +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + [self endConnectTimeout]; + + if (currentRead != nil) [self endCurrentRead]; + if (currentWrite != nil) [self endCurrentWrite]; + + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + [preBuffer reset]; + + #if TARGET_OS_IPHONE + { + if (readStream || writeStream) + { + [self removeStreamsFromRunLoop]; + + if (readStream) + { + CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL); + CFReadStreamClose(readStream); + CFRelease(readStream); + readStream = NULL; + } + if (writeStream) + { + CFWriteStreamSetClient(writeStream, kCFStreamEventNone, NULL, NULL); + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + writeStream = NULL; + } + } + } + #endif + + [sslPreBuffer reset]; + sslErrCode = lastSSLHandshakeError = noErr; + + if (sslContext) + { + // Getting a linker error here about the SSLx() functions? + // You need to add the Security Framework to your application. + + SSLClose(sslContext); + + #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) + CFRelease(sslContext); + #else + SSLDisposeContext(sslContext); + #endif + + sslContext = NULL; + } + + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't + // invoke the cancel handler if the dispatch source is paused. + // So we have to unpause the source if needed. + // This allows the cancel handler to be run, which in turn releases the source and closes the socket. + + if (!accept4Source && !accept6Source && !acceptUNSource && !readSource && !writeSource) + { + LogVerbose(@"manually closing close"); + + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"close(socket4FD)"); + close(socket4FD); + socket4FD = SOCKET_NULL; + } + + if (socket6FD != SOCKET_NULL) + { + LogVerbose(@"close(socket6FD)"); + close(socket6FD); + socket6FD = SOCKET_NULL; + } + + if (socketUN != SOCKET_NULL) + { + LogVerbose(@"close(socketUN)"); + close(socketUN); + socketUN = SOCKET_NULL; + unlink(socketUrl.path.fileSystemRepresentation); + socketUrl = nil; + } + } + else + { + if (accept4Source) + { + LogVerbose(@"dispatch_source_cancel(accept4Source)"); + dispatch_source_cancel(accept4Source); + + // We never suspend accept4Source + + accept4Source = NULL; + } + + if (accept6Source) + { + LogVerbose(@"dispatch_source_cancel(accept6Source)"); + dispatch_source_cancel(accept6Source); + + // We never suspend accept6Source + + accept6Source = NULL; + } + + if (acceptUNSource) + { + LogVerbose(@"dispatch_source_cancel(acceptUNSource)"); + dispatch_source_cancel(acceptUNSource); + + // We never suspend acceptUNSource + + acceptUNSource = NULL; + } + + if (readSource) + { + LogVerbose(@"dispatch_source_cancel(readSource)"); + dispatch_source_cancel(readSource); + + [self resumeReadSource]; + + readSource = NULL; + } + + if (writeSource) + { + LogVerbose(@"dispatch_source_cancel(writeSource)"); + dispatch_source_cancel(writeSource); + + [self resumeWriteSource]; + + writeSource = NULL; + } + + // The sockets will be closed by the cancel handlers of the corresponding source + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + socketUN = SOCKET_NULL; + } + + // If the client has passed the connect/accept method, then the connection has at least begun. + // Notify delegate that it is now ending. + BOOL shouldCallDelegate = (flags & kSocketStarted) ? YES : NO; + BOOL isDeallocating = (flags & kDealloc) ? YES : NO; + + // Clear stored socket info and all flags (config remains as is) + socketFDBytesAvailable = 0; + flags = 0; + sslWriteCachedLength = 0; + + if (shouldCallDelegate) + { + __strong id theDelegate = delegate; + __strong id theSelf = isDeallocating ? nil : self; + + if (delegateQueue && [theDelegate respondsToSelector: @selector(socketDidDisconnect:withError:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidDisconnect:theSelf withError:error]; + }}); + } + } +} + +- (void)disconnect +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + [self closeWithError:nil]; + } + }}; + + // Synchronous disconnection, as documented in the header file + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +- (void)disconnectAfterReading +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterReads); + [self maybeClose]; + } + }}); +} + +- (void)disconnectAfterWriting +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterWrites); + [self maybeClose]; + } + }}); +} + +- (void)disconnectAfterReadingAndWriting +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites); + [self maybeClose]; + } + }}); +} + +/** + * Closes the socket if possible. + * That is, if all writes have completed, and we're set to disconnect after writing, + * or if all reads have completed, and we're set to disconnect after reading. +**/ +- (void)maybeClose +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + BOOL shouldClose = NO; + + if (flags & kDisconnectAfterReads) + { + if (([readQueue count] == 0) && (currentRead == nil)) + { + if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + shouldClose = YES; + } + } + else + { + shouldClose = YES; + } + } + } + else if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + shouldClose = YES; + } + } + + if (shouldClose) + { + [self closeWithError:nil]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Errors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSError *)badConfigError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadConfigError userInfo:userInfo]; +} + +- (NSError *)badParamError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo]; +} + ++ (NSError *)gaiError:(int)gai_error +{ + NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; +} + +- (NSError *)errnoErrorWithReason:(NSString *)reason +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:errMsg, NSLocalizedDescriptionKey, + reason, NSLocalizedFailureReasonErrorKey, nil]; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +- (NSError *)errnoError +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +- (NSError *)sslError:(OSStatus)ssl_error +{ + NSString *msg = @"Error code definition can be found in Apple's SecureTransport.h"; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:msg forKey:NSLocalizedRecoverySuggestionErrorKey]; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainSSL" code:ssl_error userInfo:userInfo]; +} + +- (NSError *)connectTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketConnectTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Attempt to connect to host timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketConnectTimeoutError userInfo:userInfo]; +} + +/** + * Returns a standard AsyncSocket maxed out error. +**/ +- (NSError *)readMaxedOutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadMaxedOutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Read operation reached set maximum length", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadMaxedOutError userInfo:info]; +} + +/** + * Returns a standard AsyncSocket write timeout error. +**/ +- (NSError *)readTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Read operation timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadTimeoutError userInfo:userInfo]; +} + +/** + * Returns a standard AsyncSocket write timeout error. +**/ +- (NSError *)writeTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketWriteTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Write operation timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketWriteTimeoutError userInfo:userInfo]; +} + +- (NSError *)connectionClosedError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketClosedError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Socket closed by remote peer", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketClosedError userInfo:userInfo]; +} + +- (NSError *)otherError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Diagnostics +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isDisconnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kSocketStarted) ? NO : YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isConnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kConnected) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (NSString *)connectedHost +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self connectedHostFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self connectedHostFromSocket6:socket6FD]; + + return nil; + } + else + { + __block NSString *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (socket4FD != SOCKET_NULL) + result = [self connectedHostFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self connectedHostFromSocket6:socket6FD]; + }}); + + return result; + } +} + +- (uint16_t)connectedPort +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self connectedPortFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self connectedPortFromSocket6:socket6FD]; + + return 0; + } + else + { + __block uint16_t result = 0; + + dispatch_sync(socketQueue, ^{ + // No need for autorelease pool + + if (socket4FD != SOCKET_NULL) + result = [self connectedPortFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self connectedPortFromSocket6:socket6FD]; + }); + + return result; + } +} + +- (NSURL *)connectedUrl +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socketUN != SOCKET_NULL) + return [self connectedUrlFromSocketUN:socketUN]; + + return nil; + } + else + { + __block NSURL *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (socketUN != SOCKET_NULL) + result = [self connectedUrlFromSocketUN:socketUN]; + }}); + + return result; + } +} + +- (NSString *)localHost +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self localHostFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self localHostFromSocket6:socket6FD]; + + return nil; + } + else + { + __block NSString *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (socket4FD != SOCKET_NULL) + result = [self localHostFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self localHostFromSocket6:socket6FD]; + }}); + + return result; + } +} + +- (uint16_t)localPort +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self localPortFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self localPortFromSocket6:socket6FD]; + + return 0; + } + else + { + __block uint16_t result = 0; + + dispatch_sync(socketQueue, ^{ + // No need for autorelease pool + + if (socket4FD != SOCKET_NULL) + result = [self localPortFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self localPortFromSocket6:socket6FD]; + }); + + return result; + } +} + +- (NSString *)connectedHost4 +{ + if (socket4FD != SOCKET_NULL) + return [self connectedHostFromSocket4:socket4FD]; + + return nil; +} + +- (NSString *)connectedHost6 +{ + if (socket6FD != SOCKET_NULL) + return [self connectedHostFromSocket6:socket6FD]; + + return nil; +} + +- (uint16_t)connectedPort4 +{ + if (socket4FD != SOCKET_NULL) + return [self connectedPortFromSocket4:socket4FD]; + + return 0; +} + +- (uint16_t)connectedPort6 +{ + if (socket6FD != SOCKET_NULL) + return [self connectedPortFromSocket6:socket6FD]; + + return 0; +} + +- (NSString *)localHost4 +{ + if (socket4FD != SOCKET_NULL) + return [self localHostFromSocket4:socket4FD]; + + return nil; +} + +- (NSString *)localHost6 +{ + if (socket6FD != SOCKET_NULL) + return [self localHostFromSocket6:socket6FD]; + + return nil; +} + +- (uint16_t)localPort4 +{ + if (socket4FD != SOCKET_NULL) + return [self localPortFromSocket4:socket4FD]; + + return 0; +} + +- (uint16_t)localPort6 +{ + if (socket6FD != SOCKET_NULL) + return [self localPortFromSocket6:socket6FD]; + + return 0; +} + +- (NSString *)connectedHostFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr4:&sockaddr4]; +} + +- (NSString *)connectedHostFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr6:&sockaddr6]; +} + +- (uint16_t)connectedPortFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr4:&sockaddr4]; +} + +- (uint16_t)connectedPortFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr6:&sockaddr6]; +} + +- (NSURL *)connectedUrlFromSocketUN:(int)socketFD +{ + struct sockaddr_un sockaddr; + socklen_t sockaddrlen = sizeof(sockaddr); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr, &sockaddrlen) < 0) + { + return 0; + } + return [[self class] urlFromSockaddrUN:&sockaddr]; +} + +- (NSString *)localHostFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr4:&sockaddr4]; +} + +- (NSString *)localHostFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr6:&sockaddr6]; +} + +- (uint16_t)localPortFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr4:&sockaddr4]; +} + +- (uint16_t)localPortFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr6:&sockaddr6]; +} + +- (NSData *)connectedAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + if (socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; + } + } + + if (socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; + } + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (NSData *)localAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + if (socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; + } + } + + if (socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; + } + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isIPv4 +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (socket4FD != SOCKET_NULL); + } + else + { + __block BOOL result = NO; + + dispatch_sync(socketQueue, ^{ + result = (socket4FD != SOCKET_NULL); + }); + + return result; + } +} + +- (BOOL)isIPv6 +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (socket6FD != SOCKET_NULL); + } + else + { + __block BOOL result = NO; + + dispatch_sync(socketQueue, ^{ + result = (socket6FD != SOCKET_NULL); + }); + + return result; + } +} + +- (BOOL)isSecure +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (flags & kSocketSecure) ? YES : NO; + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = (flags & kSocketSecure) ? YES : NO; + }); + + return result; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Finds the address of an interface description. + * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34). + * + * The interface description may optionally contain a port number at the end, separated by a colon. + * If a non-zero port parameter is provided, any port number in the interface description is ignored. + * + * The returned value is a 'struct sockaddr' wrapped in an NSMutableData object. +**/ +- (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr + address6:(NSMutableData **)interfaceAddr6Ptr + fromDescription:(NSString *)interfaceDescription + port:(uint16_t)port +{ + NSMutableData *addr4 = nil; + NSMutableData *addr6 = nil; + + NSString *interface = nil; + + NSArray *components = [interfaceDescription componentsSeparatedByString:@":"]; + if ([components count] > 0) + { + NSString *temp = [components objectAtIndex:0]; + if ([temp length] > 0) + { + interface = temp; + } + } + if ([components count] > 1 && port == 0) + { + long portL = strtol([[components objectAtIndex:1] UTF8String], NULL, 10); + + if (portL > 0 && portL <= UINT16_MAX) + { + port = (uint16_t)portL; + } + } + + if (interface == nil) + { + // ANY address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_any; + + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"]) + { + // LOOPBACK address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_loopback; + + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else + { + const char *iface = [interface UTF8String]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) + { + // IPv4 + + struct sockaddr_in nativeAddr4; + memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4)); + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + nativeAddr4.sin_port = htons(port); + + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + else + { + char ip[INET_ADDRSTRLEN]; + + const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + nativeAddr4.sin_port = htons(port); + + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + } + } + else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) + { + // IPv6 + + struct sockaddr_in6 nativeAddr6; + memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6)); + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + else + { + char ip[INET6_ADDRSTRLEN]; + + const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + } + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + } + + if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; + if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; +} + +- (NSData *)getInterfaceAddressFromUrl:(NSURL *)url; +{ + NSString *path = url.path; + if (path.length == 0) { + return nil; + } + + struct sockaddr_un nativeAddr; + nativeAddr.sun_family = AF_UNIX; + strlcpy(nativeAddr.sun_path, path.fileSystemRepresentation, sizeof(nativeAddr.sun_path)); + nativeAddr.sun_len = SUN_LEN(&nativeAddr); + NSData *interface = [NSData dataWithBytes:&nativeAddr length:sizeof(struct sockaddr_un)]; + + return interface; +} + +- (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD +{ + readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue); + writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, socketQueue); + + // Setup event handlers + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"readEventBlock"); + + strongSelf->socketFDBytesAvailable = dispatch_source_get_data(strongSelf->readSource); + LogVerbose(@"socketFDBytesAvailable: %lu", strongSelf->socketFDBytesAvailable); + + if (strongSelf->socketFDBytesAvailable > 0) + [strongSelf doReadData]; + else + [strongSelf doReadEOF]; + + #pragma clang diagnostic pop + }}); + + dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"writeEventBlock"); + + strongSelf->flags |= kSocketCanAcceptBytes; + [strongSelf doWriteData]; + + #pragma clang diagnostic pop + }}); + + // Setup cancel handlers + + __block int socketFDRefCount = 2; + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theReadSource = readSource; + dispatch_source_t theWriteSource = writeSource; + #endif + + dispatch_source_set_cancel_handler(readSource, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"readCancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(readSource)"); + dispatch_release(theReadSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socketFD)"); + close(socketFD); + } + + #pragma clang diagnostic pop + }); + + dispatch_source_set_cancel_handler(writeSource, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"writeCancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(writeSource)"); + dispatch_release(theWriteSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socketFD)"); + close(socketFD); + } + + #pragma clang diagnostic pop + }); + + // We will not be able to read until data arrives. + // But we should be able to write immediately. + + socketFDBytesAvailable = 0; + flags &= ~kReadSourceSuspended; + + LogVerbose(@"dispatch_resume(readSource)"); + dispatch_resume(readSource); + + flags |= kSocketCanAcceptBytes; + flags |= kWriteSourceSuspended; +} + +- (BOOL)usingCFStreamForTLS +{ + #if TARGET_OS_IPHONE + + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) + { + // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. + + return YES; + } + + #endif + + return NO; +} + +- (BOOL)usingSecureTransportForTLS +{ + // Invoking this method is equivalent to ![self usingCFStreamForTLS] (just more readable) + + #if TARGET_OS_IPHONE + + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) + { + // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. + + return NO; + } + + #endif + + return YES; +} + +- (void)suspendReadSource +{ + if (!(flags & kReadSourceSuspended)) + { + LogVerbose(@"dispatch_suspend(readSource)"); + + dispatch_suspend(readSource); + flags |= kReadSourceSuspended; + } +} + +- (void)resumeReadSource +{ + if (flags & kReadSourceSuspended) + { + LogVerbose(@"dispatch_resume(readSource)"); + + dispatch_resume(readSource); + flags &= ~kReadSourceSuspended; + } +} + +- (void)suspendWriteSource +{ + if (!(flags & kWriteSourceSuspended)) + { + LogVerbose(@"dispatch_suspend(writeSource)"); + + dispatch_suspend(writeSource); + flags |= kWriteSourceSuspended; + } +} + +- (void)resumeWriteSource +{ + if (flags & kWriteSourceSuspended) + { + LogVerbose(@"dispatch_resume(writeSource)"); + + dispatch_resume(writeSource); + flags &= ~kWriteSourceSuspended; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Reading +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag +{ + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:length + timeout:timeout + readLength:0 + terminator:nil + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToLength:length withTimeout:timeout buffer:nil bufferOffset:0 tag:tag]; +} + +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + if (length == 0) { + LogWarn(@"Cannot read: length == 0"); + return; + } + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:0 + timeout:timeout + readLength:length + terminator:nil + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:length tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)maxLength + tag:(long)tag +{ + if ([data length] == 0) { + LogWarn(@"Cannot read: [data length] == 0"); + return; + } + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + if (maxLength > 0 && maxLength < [data length]) { + LogWarn(@"Cannot read: maxLength > 0 && maxLength < [data length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:maxLength + timeout:timeout + readLength:0 + terminator:data + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +{ + __block float result = 0.0F; + + dispatch_block_t block = ^{ + + if (!currentRead || ![currentRead isKindOfClass:[GCDAsyncReadPacket class]]) + { + // We're not reading anything right now. + + if (tagPtr != NULL) *tagPtr = 0; + if (donePtr != NULL) *donePtr = 0; + if (totalPtr != NULL) *totalPtr = 0; + + result = NAN; + } + else + { + // It's only possible to know the progress of our read if we're reading to a certain length. + // If we're reading to data, we of course have no idea when the data will arrive. + // If we're reading to timeout, then we have no idea when the next chunk of data will arrive. + + NSUInteger done = currentRead->bytesDone; + NSUInteger total = currentRead->readLength; + + if (tagPtr != NULL) *tagPtr = currentRead->tag; + if (donePtr != NULL) *donePtr = done; + if (totalPtr != NULL) *totalPtr = total; + + if (total > 0) + result = (float)done / (float)total; + else + result = 1.0F; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +/** + * This method starts a new read, if needed. + * + * It is called when: + * - a user requests a read + * - after a read request has finished (to handle the next request) + * - immediately after the socket opens to handle any pending requests + * + * This method also handles auto-disconnect post read/write completion. +**/ +- (void)maybeDequeueRead +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + // If we're not currently processing a read AND we have an available read stream + if ((currentRead == nil) && (flags & kConnected)) + { + if ([readQueue count] > 0) + { + // Dequeue the next object in the write queue + currentRead = [readQueue objectAtIndex:0]; + [readQueue removeObjectAtIndex:0]; + + + if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]]) + { + LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); + + // Attempt to start TLS + flags |= kStartingReadTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + LogVerbose(@"Dequeued GCDAsyncReadPacket"); + + // Setup read timer (if needed) + [self setupReadTimerWithTimeout:currentRead->timeout]; + + // Immediately read, if possible + [self doReadData]; + } + } + else if (flags & kDisconnectAfterReads) + { + if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + [self closeWithError:nil]; + } + } + else + { + [self closeWithError:nil]; + } + } + else if (flags & kSocketSecure) + { + [self flushSSLBuffers]; + + // Edge case: + // + // We just drained all data from the ssl buffers, + // and all known data from the socket (socketFDBytesAvailable). + // + // If we didn't get any data from this process, + // then we may have reached the end of the TCP stream. + // + // Be sure callbacks are enabled so we're notified about a disconnection. + + if ([preBuffer availableBytes] == 0) + { + if ([self usingCFStreamForTLS]) { + // Callbacks never disabled + } + else { + [self resumeReadSource]; + } + } + } + } +} + +- (void)flushSSLBuffers +{ + LogTrace(); + + NSAssert((flags & kSocketSecure), @"Cannot flush ssl buffers on non-secure socket"); + + if ([preBuffer availableBytes] > 0) + { + // Only flush the ssl buffers if the prebuffer is empty. + // This is to avoid growing the prebuffer inifinitely large. + + return; + } + + #if TARGET_OS_IPHONE + + if ([self usingCFStreamForTLS]) + { + if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) + { + LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); + + CFIndex defaultBytesToRead = (1024 * 4); + + [preBuffer ensureCapacityForWrite:defaultBytesToRead]; + + uint8_t *buffer = [preBuffer writeBuffer]; + + CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead); + LogVerbose(@"%@ - CFReadStreamRead(): result = %i", THIS_METHOD, (int)result); + + if (result > 0) + { + [preBuffer didWrite:result]; + } + + flags &= ~kSecureSocketHasBytesAvailable; + } + + return; + } + + #endif + + __block NSUInteger estimatedBytesAvailable = 0; + + dispatch_block_t updateEstimatedBytesAvailable = ^{ + + // Figure out if there is any data available to be read + // + // socketFDBytesAvailable <- Number of encrypted bytes we haven't read from the bsd socket + // [sslPreBuffer availableBytes] <- Number of encrypted bytes we've buffered from bsd socket + // sslInternalBufSize <- Number of decrypted bytes SecureTransport has buffered + // + // We call the variable "estimated" because we don't know how many decrypted bytes we'll get + // from the encrypted bytes in the sslPreBuffer. + // However, we do know this is an upper bound on the estimation. + + estimatedBytesAvailable = socketFDBytesAvailable + [sslPreBuffer availableBytes]; + + size_t sslInternalBufSize = 0; + SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); + + estimatedBytesAvailable += sslInternalBufSize; + }; + + updateEstimatedBytesAvailable(); + + if (estimatedBytesAvailable > 0) + { + LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); + + BOOL done = NO; + do + { + LogVerbose(@"%@ - estimatedBytesAvailable = %lu", THIS_METHOD, (unsigned long)estimatedBytesAvailable); + + // Make sure there's enough room in the prebuffer + + [preBuffer ensureCapacityForWrite:estimatedBytesAvailable]; + + // Read data into prebuffer + + uint8_t *buffer = [preBuffer writeBuffer]; + size_t bytesRead = 0; + + OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead); + LogVerbose(@"%@ - read from secure socket = %u", THIS_METHOD, (unsigned)bytesRead); + + if (bytesRead > 0) + { + [preBuffer didWrite:bytesRead]; + } + + LogVerbose(@"%@ - prebuffer.length = %zu", THIS_METHOD, [preBuffer availableBytes]); + + if (result != noErr) + { + done = YES; + } + else + { + updateEstimatedBytesAvailable(); + } + + } while (!done && estimatedBytesAvailable > 0); + } +} + +- (void)doReadData +{ + LogTrace(); + + // This method is called on the socketQueue. + // It might be called directly, or via the readSource when data is available to be read. + + if ((currentRead == nil) || (flags & kReadsPaused)) + { + LogVerbose(@"No currentRead or kReadsPaused"); + + // Unable to read at this time + + if (flags & kSocketSecure) + { + // Here's the situation: + // + // We have an established secure connection. + // There may not be a currentRead, but there might be encrypted data sitting around for us. + // When the user does get around to issuing a read, that encrypted data will need to be decrypted. + // + // So why make the user wait? + // We might as well get a head start on decrypting some data now. + // + // The other reason we do this has to do with detecting a socket disconnection. + // The SSL/TLS protocol has it's own disconnection handshake. + // So when a secure socket is closed, a "goodbye" packet comes across the wire. + // We want to make sure we read the "goodbye" packet so we can properly detect the TCP disconnection. + + [self flushSSLBuffers]; + } + + if ([self usingCFStreamForTLS]) + { + // CFReadStream only fires once when there is available data. + // It won't fire again until we've invoked CFReadStreamRead. + } + else + { + // If the readSource is firing, we need to pause it + // or else it will continue to fire over and over again. + // + // If the readSource is not firing, + // we want it to continue monitoring the socket. + + if (socketFDBytesAvailable > 0) + { + [self suspendReadSource]; + } + } + return; + } + + BOOL hasBytesAvailable = NO; + unsigned long estimatedBytesAvailable = 0; + + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // Requested CFStream, rather than SecureTransport, for TLS (via GCDAsyncSocketUseCFStreamForTLS) + + estimatedBytesAvailable = 0; + if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) + hasBytesAvailable = YES; + else + hasBytesAvailable = NO; + + #endif + } + else + { + estimatedBytesAvailable = socketFDBytesAvailable; + + if (flags & kSocketSecure) + { + // There are 2 buffers to be aware of here. + // + // We are using SecureTransport, a TLS/SSL security layer which sits atop TCP. + // We issue a read to the SecureTranport API, which in turn issues a read to our SSLReadFunction. + // Our SSLReadFunction then reads from the BSD socket and returns the encrypted data to SecureTransport. + // SecureTransport then decrypts the data, and finally returns the decrypted data back to us. + // + // The first buffer is one we create. + // SecureTransport often requests small amounts of data. + // This has to do with the encypted packets that are coming across the TCP stream. + // But it's non-optimal to do a bunch of small reads from the BSD socket. + // So our SSLReadFunction reads all available data from the socket (optimizing the sys call) + // and may store excess in the sslPreBuffer. + + estimatedBytesAvailable += [sslPreBuffer availableBytes]; + + // The second buffer is within SecureTransport. + // As mentioned earlier, there are encrypted packets coming across the TCP stream. + // SecureTransport needs the entire packet to decrypt it. + // But if the entire packet produces X bytes of decrypted data, + // and we only asked SecureTransport for X/2 bytes of data, + // it must store the extra X/2 bytes of decrypted data for the next read. + // + // The SSLGetBufferedReadSize function will tell us the size of this internal buffer. + // From the documentation: + // + // "This function does not block or cause any low-level read operations to occur." + + size_t sslInternalBufSize = 0; + SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); + + estimatedBytesAvailable += sslInternalBufSize; + } + + hasBytesAvailable = (estimatedBytesAvailable > 0); + } + + if ((hasBytesAvailable == NO) && ([preBuffer availableBytes] == 0)) + { + LogVerbose(@"No data available to read..."); + + // No data available to read. + + if (![self usingCFStreamForTLS]) + { + // Need to wait for readSource to fire and notify us of + // available data in the socket's internal read buffer. + + [self resumeReadSource]; + } + return; + } + + if (flags & kStartingReadTLS) + { + LogVerbose(@"Waiting for SSL/TLS handshake to complete"); + + // The readQueue is waiting for SSL/TLS handshake to complete. + + if (flags & kStartingWriteTLS) + { + if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) + { + // We are in the process of a SSL Handshake. + // We were waiting for incoming data which has just arrived. + + [self ssl_continueSSLHandshake]; + } + } + else + { + // We are still waiting for the writeQueue to drain and start the SSL/TLS process. + // We now know data is available to read. + + if (![self usingCFStreamForTLS]) + { + // Suspend the read source or else it will continue to fire nonstop. + + [self suspendReadSource]; + } + } + + return; + } + + BOOL done = NO; // Completed read operation + NSError *error = nil; // Error occurred + + NSUInteger totalBytesReadForCurrentRead = 0; + + // + // STEP 1 - READ FROM PREBUFFER + // + + if ([preBuffer availableBytes] > 0) + { + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + NSUInteger bytesToCopy; + + if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; + } + else + { + // Read type #1 or #2 + + bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]]; + } + + // Make sure we have enough room in the buffer for our read. + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; + + // Copy bytes from prebuffer into packet buffer + + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(buffer, [preBuffer readBuffer], bytesToCopy); + + // Remove the copied bytes from the preBuffer + [preBuffer didRead:bytesToCopy]; + + LogVerbose(@"copied(%lu) preBufferLength(%zu)", (unsigned long)bytesToCopy, [preBuffer availableBytes]); + + // Update totals + + currentRead->bytesDone += bytesToCopy; + totalBytesReadForCurrentRead += bytesToCopy; + + // Check to see if the read operation is done + + if (currentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + + done = (currentRead->bytesDone == currentRead->readLength); + } + else if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method + + if (!done && currentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if (currentRead->bytesDone >= currentRead->maxLength) + { + error = [self readMaxedOutError]; + } + } + } + else + { + // Read type #1 - read all available data + // + // We're done as soon as + // - we've read all available data (in prebuffer and socket) + // - we've read the maxLength of read packet. + + done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength)); + } + + } + + // + // STEP 2 - READ FROM SOCKET + // + + BOOL socketEOF = (flags & kSocketHasReadEOF) ? YES : NO; // Nothing more to read via socket (end of file) + BOOL waiting = !done && !error && !socketEOF && !hasBytesAvailable; // Ran out of data, waiting for more + + if (!done && !error && !socketEOF && hasBytesAvailable) + { + NSAssert(([preBuffer availableBytes] == 0), @"Invalid logic"); + + BOOL readIntoPreBuffer = NO; + uint8_t *buffer = NULL; + size_t bytesRead = 0; + + if (flags & kSocketSecure) + { + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // Using CFStream, rather than SecureTransport, for TLS + + NSUInteger defaultReadLength = (1024 * 32); + + NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength + shouldPreBuffer:&readIntoPreBuffer]; + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // Read data into buffer + + CFIndex result = CFReadStreamRead(readStream, buffer, (CFIndex)bytesToRead); + LogVerbose(@"CFReadStreamRead(): result = %i", (int)result); + + if (result < 0) + { + error = (__bridge_transfer NSError *)CFReadStreamCopyError(readStream); + } + else if (result == 0) + { + socketEOF = YES; + } + else + { + waiting = YES; + bytesRead = (size_t)result; + } + + // We only know how many decrypted bytes were read. + // The actual number of bytes read was likely more due to the overhead of the encryption. + // So we reset our flag, and rely on the next callback to alert us of more data. + flags &= ~kSecureSocketHasBytesAvailable; + + #endif + } + else + { + // Using SecureTransport for TLS + // + // We know: + // - how many bytes are available on the socket + // - how many encrypted bytes are sitting in the sslPreBuffer + // - how many decypted bytes are sitting in the sslContext + // + // But we do NOT know: + // - how many encypted bytes are sitting in the sslContext + // + // So we play the regular game of using an upper bound instead. + + NSUInteger defaultReadLength = (1024 * 32); + + if (defaultReadLength < estimatedBytesAvailable) { + defaultReadLength = estimatedBytesAvailable + (1024 * 16); + } + + NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength + shouldPreBuffer:&readIntoPreBuffer]; + + if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t + bytesToRead = SIZE_MAX; + } + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // The documentation from Apple states: + // + // "a read operation might return errSSLWouldBlock, + // indicating that less data than requested was actually transferred" + // + // However, starting around 10.7, the function will sometimes return noErr, + // even if it didn't read as much data as requested. So we need to watch out for that. + + OSStatus result; + do + { + void *loop_buffer = buffer + bytesRead; + size_t loop_bytesToRead = (size_t)bytesToRead - bytesRead; + size_t loop_bytesRead = 0; + + result = SSLRead(sslContext, loop_buffer, loop_bytesToRead, &loop_bytesRead); + LogVerbose(@"read from secure socket = %u", (unsigned)loop_bytesRead); + + bytesRead += loop_bytesRead; + + } while ((result == noErr) && (bytesRead < bytesToRead)); + + + if (result != noErr) + { + if (result == errSSLWouldBlock) + waiting = YES; + else + { + if (result == errSSLClosedGraceful || result == errSSLClosedAbort) + { + // We've reached the end of the stream. + // Handle this the same way we would an EOF from the socket. + socketEOF = YES; + sslErrCode = result; + } + else + { + error = [self sslError:result]; + } + } + // It's possible that bytesRead > 0, even if the result was errSSLWouldBlock. + // This happens when the SSLRead function is able to read some data, + // but not the entire amount we requested. + + if (bytesRead <= 0) + { + bytesRead = 0; + } + } + + // Do not modify socketFDBytesAvailable. + // It will be updated via the SSLReadFunction(). + } + } + else + { + // Normal socket operation + + NSUInteger bytesToRead; + + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable + shouldPreBuffer:&readIntoPreBuffer]; + } + else + { + // Read type #1 or #2 + + bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable]; + } + + if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t (read param 3) + bytesToRead = SIZE_MAX; + } + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // Read data into buffer + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + ssize_t result = read(socketFD, buffer, (size_t)bytesToRead); + LogVerbose(@"read from socket = %i", (int)result); + + if (result < 0) + { + if (errno == EWOULDBLOCK) + waiting = YES; + else + error = [self errnoErrorWithReason:@"Error in read() function"]; + + socketFDBytesAvailable = 0; + } + else if (result == 0) + { + socketEOF = YES; + socketFDBytesAvailable = 0; + } + else + { + bytesRead = result; + + if (bytesRead < bytesToRead) + { + // The read returned less data than requested. + // This means socketFDBytesAvailable was a bit off due to timing, + // because we read from the socket right when the readSource event was firing. + socketFDBytesAvailable = 0; + } + else + { + if (socketFDBytesAvailable <= bytesRead) + socketFDBytesAvailable = 0; + else + socketFDBytesAvailable -= bytesRead; + } + + if (socketFDBytesAvailable == 0) + { + waiting = YES; + } + } + } + + if (bytesRead > 0) + { + // Check to see if the read operation is done + + if (currentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + // + // Note: We should never be using a prebuffer when we're reading a specific length of data. + + NSAssert(readIntoPreBuffer == NO, @"Invalid logic"); + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + + done = (currentRead->bytesDone == currentRead->readLength); + } + else if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + if (readIntoPreBuffer) + { + // We just read a big chunk of data into the preBuffer + + [preBuffer didWrite:bytesRead]; + LogVerbose(@"read data into preBuffer - preBuffer.length = %zu", [preBuffer availableBytes]); + + // Search for the terminating sequence + + NSUInteger bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; + LogVerbose(@"copying %lu bytes from preBuffer", (unsigned long)bytesToCopy); + + // Ensure there's room on the read packet's buffer + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; + + // Copy bytes from prebuffer into read buffer + + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(readBuf, [preBuffer readBuffer], bytesToCopy); + + // Remove the copied bytes from the prebuffer + [preBuffer didRead:bytesToCopy]; + LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); + + // Update totals + currentRead->bytesDone += bytesToCopy; + totalBytesReadForCurrentRead += bytesToCopy; + + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method above + } + else + { + // We just read a big chunk of data directly into the packet's buffer. + // We need to move any overflow into the prebuffer. + + NSInteger overflow = [currentRead searchForTermAfterPreBuffering:bytesRead]; + + if (overflow == 0) + { + // Perfect match! + // Every byte we read stays in the read buffer, + // and the last byte we read was the last byte of the term. + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + done = YES; + } + else if (overflow > 0) + { + // The term was found within the data that we read, + // and there are extra bytes that extend past the end of the term. + // We need to move these excess bytes out of the read packet and into the prebuffer. + + NSInteger underflow = bytesRead - overflow; + + // Copy excess data into preBuffer + + LogVerbose(@"copying %ld overflow bytes into preBuffer", (long)overflow); + [preBuffer ensureCapacityForWrite:overflow]; + + uint8_t *overflowBuffer = buffer + underflow; + memcpy([preBuffer writeBuffer], overflowBuffer, overflow); + + [preBuffer didWrite:overflow]; + LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); + + // Note: The completeCurrentRead method will trim the buffer for us. + + currentRead->bytesDone += underflow; + totalBytesReadForCurrentRead += underflow; + done = YES; + } + else + { + // The term was not found within the data that we read. + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + done = NO; + } + } + + if (!done && currentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if (currentRead->bytesDone >= currentRead->maxLength) + { + error = [self readMaxedOutError]; + } + } + } + else + { + // Read type #1 - read all available data + + if (readIntoPreBuffer) + { + // We just read a chunk of data into the preBuffer + + [preBuffer didWrite:bytesRead]; + + // Now copy the data into the read packet. + // + // Recall that we didn't read directly into the packet's buffer to avoid + // over-allocating memory since we had no clue how much data was available to be read. + // + // Ensure there's room on the read packet's buffer + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesRead]; + + // Copy bytes from prebuffer into read buffer + + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(readBuf, [preBuffer readBuffer], bytesRead); + + // Remove the copied bytes from the prebuffer + [preBuffer didRead:bytesRead]; + + // Update totals + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + } + else + { + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + } + + done = YES; + } + + } // if (bytesRead > 0) + + } // if (!done && !error && !socketEOF && hasBytesAvailable) + + + if (!done && currentRead->readLength == 0 && currentRead->term == nil) + { + // Read type #1 - read all available data + // + // We might arrive here if we read data from the prebuffer but not from the socket. + + done = (totalBytesReadForCurrentRead > 0); + } + + // Check to see if we're done, or if we've made progress + + if (done) + { + [self completeCurrentRead]; + + if (!error && (!socketEOF || [preBuffer availableBytes] > 0)) + { + [self maybeDequeueRead]; + } + } + else if (totalBytesReadForCurrentRead > 0) + { + // We're not done read type #2 or #3 yet, but we have read in some bytes + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)]) + { + long theReadTag = currentRead->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theReadTag]; + }}); + } + } + + // Check for errors + + if (error) + { + [self closeWithError:error]; + } + else if (socketEOF) + { + [self doReadEOF]; + } + else if (waiting) + { + if (![self usingCFStreamForTLS]) + { + // Monitor the socket for readability (if we're not already doing so) + [self resumeReadSource]; + } + } + + // Do not add any code here without first adding return statements in the error cases above. +} + +- (void)doReadEOF +{ + LogTrace(); + + // This method may be called more than once. + // If the EOF is read while there is still data in the preBuffer, + // then this method may be called continually after invocations of doReadData to see if it's time to disconnect. + + flags |= kSocketHasReadEOF; + + if (flags & kSocketSecure) + { + // If the SSL layer has any buffered data, flush it into the preBuffer now. + + [self flushSSLBuffers]; + } + + BOOL shouldDisconnect = NO; + NSError *error = nil; + + if ((flags & kStartingReadTLS) || (flags & kStartingWriteTLS)) + { + // We received an EOF during or prior to startTLS. + // The SSL/TLS handshake is now impossible, so this is an unrecoverable situation. + + shouldDisconnect = YES; + + if ([self usingSecureTransportForTLS]) + { + error = [self sslError:errSSLClosedAbort]; + } + } + else if (flags & kReadStreamClosed) + { + // The preBuffer has already been drained. + // The config allows half-duplex connections. + // We've previously checked the socket, and it appeared writeable. + // So we marked the read stream as closed and notified the delegate. + // + // As per the half-duplex contract, the socket will be closed when a write fails, + // or when the socket is manually closed. + + shouldDisconnect = NO; + } + else if ([preBuffer availableBytes] > 0) + { + LogVerbose(@"Socket reached EOF, but there is still data available in prebuffer"); + + // Although we won't be able to read any more data from the socket, + // there is existing data that has been prebuffered that we can read. + + shouldDisconnect = NO; + } + else if (config & kAllowHalfDuplexConnection) + { + // We just received an EOF (end of file) from the socket's read stream. + // This means the remote end of the socket (the peer we're connected to) + // has explicitly stated that it will not be sending us any more data. + // + // Query the socket to see if it is still writeable. (Perhaps the peer will continue reading data from us) + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + struct pollfd pfd[1]; + pfd[0].fd = socketFD; + pfd[0].events = POLLOUT; + pfd[0].revents = 0; + + poll(pfd, 1, 0); + + if (pfd[0].revents & POLLOUT) + { + // Socket appears to still be writeable + + shouldDisconnect = NO; + flags |= kReadStreamClosed; + + // Notify the delegate that we're going half-duplex + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidCloseReadStream:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidCloseReadStream:self]; + }}); + } + } + else + { + shouldDisconnect = YES; + } + } + else + { + shouldDisconnect = YES; + } + + + if (shouldDisconnect) + { + if (error == nil) + { + if ([self usingSecureTransportForTLS]) + { + if (sslErrCode != noErr && sslErrCode != errSSLClosedGraceful) + { + error = [self sslError:sslErrCode]; + } + else + { + error = [self connectionClosedError]; + } + } + else + { + error = [self connectionClosedError]; + } + } + [self closeWithError:error]; + } + else + { + if (![self usingCFStreamForTLS]) + { + // Suspend the read source (if needed) + + [self suspendReadSource]; + } + } +} + +- (void)completeCurrentRead +{ + LogTrace(); + + NSAssert(currentRead, @"Trying to complete current read when there is no current read."); + + + NSData *result = nil; + + if (currentRead->bufferOwner) + { + // We created the buffer on behalf of the user. + // Trim our buffer to be the proper size. + [currentRead->buffer setLength:currentRead->bytesDone]; + + result = currentRead->buffer; + } + else + { + // We did NOT create the buffer. + // The buffer is owned by the caller. + // Only trim the buffer if we had to increase its size. + + if ([currentRead->buffer length] > currentRead->originalBufferLength) + { + NSUInteger readSize = currentRead->startOffset + currentRead->bytesDone; + NSUInteger origSize = currentRead->originalBufferLength; + + NSUInteger buffSize = MAX(readSize, origSize); + + [currentRead->buffer setLength:buffSize]; + } + + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset; + + result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO]; + } + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadData:withTag:)]) + { + GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReadData:result withTag:theRead->tag]; + }}); + } + + [self endCurrentRead]; +} + +- (void)endCurrentRead +{ + if (readTimer) + { + dispatch_source_cancel(readTimer); + readTimer = NULL; + } + + currentRead = nil; +} + +- (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doReadTimeout]; + + #pragma clang diagnostic pop + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theReadTimer = readTimer; + dispatch_source_set_cancel_handler(readTimer, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(readTimer)"); + dispatch_release(theReadTimer); + + #pragma clang diagnostic pop + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(readTimer); + } +} + +- (void)doReadTimeout +{ + // This is a little bit tricky. + // Ideally we'd like to synchronously query the delegate about a timeout extension. + // But if we do so synchronously we risk a possible deadlock. + // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. + + flags |= kReadsPaused; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutReadWithTag:elapsed:bytesDone:)]) + { + GCDAsyncReadPacket *theRead = currentRead; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + NSTimeInterval timeoutExtension = 0.0; + + timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag + elapsed:theRead->timeout + bytesDone:theRead->bytesDone]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self doReadTimeoutWithExtension:timeoutExtension]; + }}); + }}); + } + else + { + [self doReadTimeoutWithExtension:0.0]; + } +} + +- (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension +{ + if (currentRead) + { + if (timeoutExtension > 0.0) + { + currentRead->timeout += timeoutExtension; + + // Reschedule the timer + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); + + // Unpause reads, and continue + flags &= ~kReadsPaused; + [self doReadData]; + } + else + { + LogVerbose(@"ReadTimeout"); + + [self closeWithError:[self readTimeoutError]]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Writing +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + if ([data length] == 0) return; + + GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [writeQueue addObject:packet]; + [self maybeDequeueWrite]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +{ + __block float result = 0.0F; + + dispatch_block_t block = ^{ + + if (!currentWrite || ![currentWrite isKindOfClass:[GCDAsyncWritePacket class]]) + { + // We're not writing anything right now. + + if (tagPtr != NULL) *tagPtr = 0; + if (donePtr != NULL) *donePtr = 0; + if (totalPtr != NULL) *totalPtr = 0; + + result = NAN; + } + else + { + NSUInteger done = currentWrite->bytesDone; + NSUInteger total = [currentWrite->buffer length]; + + if (tagPtr != NULL) *tagPtr = currentWrite->tag; + if (donePtr != NULL) *donePtr = done; + if (totalPtr != NULL) *totalPtr = total; + + result = (float)done / (float)total; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +/** + * Conditionally starts a new write. + * + * It is called when: + * - a user requests a write + * - after a write request has finished (to handle the next request) + * - immediately after the socket opens to handle any pending requests + * + * This method also handles auto-disconnect post read/write completion. +**/ +- (void)maybeDequeueWrite +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + // If we're not currently processing a write AND we have an available write stream + if ((currentWrite == nil) && (flags & kConnected)) + { + if ([writeQueue count] > 0) + { + // Dequeue the next object in the write queue + currentWrite = [writeQueue objectAtIndex:0]; + [writeQueue removeObjectAtIndex:0]; + + + if ([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]]) + { + LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); + + // Attempt to start TLS + flags |= kStartingWriteTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + LogVerbose(@"Dequeued GCDAsyncWritePacket"); + + // Setup write timer (if needed) + [self setupWriteTimerWithTimeout:currentWrite->timeout]; + + // Immediately write, if possible + [self doWriteData]; + } + } + else if (flags & kDisconnectAfterWrites) + { + if (flags & kDisconnectAfterReads) + { + if (([readQueue count] == 0) && (currentRead == nil)) + { + [self closeWithError:nil]; + } + } + else + { + [self closeWithError:nil]; + } + } + } +} + +- (void)doWriteData +{ + LogTrace(); + + // This method is called by the writeSource via the socketQueue + + if ((currentWrite == nil) || (flags & kWritesPaused)) + { + LogVerbose(@"No currentWrite or kWritesPaused"); + + // Unable to write at this time + + if ([self usingCFStreamForTLS]) + { + // CFWriteStream only fires once when there is available data. + // It won't fire again until we've invoked CFWriteStreamWrite. + } + else + { + // If the writeSource is firing, we need to pause it + // or else it will continue to fire over and over again. + + if (flags & kSocketCanAcceptBytes) + { + [self suspendWriteSource]; + } + } + return; + } + + if (!(flags & kSocketCanAcceptBytes)) + { + LogVerbose(@"No space available to write..."); + + // No space available to write. + + if (![self usingCFStreamForTLS]) + { + // Need to wait for writeSource to fire and notify us of + // available space in the socket's internal write buffer. + + [self resumeWriteSource]; + } + return; + } + + if (flags & kStartingWriteTLS) + { + LogVerbose(@"Waiting for SSL/TLS handshake to complete"); + + // The writeQueue is waiting for SSL/TLS handshake to complete. + + if (flags & kStartingReadTLS) + { + if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) + { + // We are in the process of a SSL Handshake. + // We were waiting for available space in the socket's internal OS buffer to continue writing. + + [self ssl_continueSSLHandshake]; + } + } + else + { + // We are still waiting for the readQueue to drain and start the SSL/TLS process. + // We now know we can write to the socket. + + if (![self usingCFStreamForTLS]) + { + // Suspend the write source or else it will continue to fire nonstop. + + [self suspendWriteSource]; + } + } + + return; + } + + // Note: This method is not called if currentWrite is a GCDAsyncSpecialPacket (startTLS packet) + + BOOL waiting = NO; + NSError *error = nil; + size_t bytesWritten = 0; + + if (flags & kSocketSecure) + { + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // + // Writing data using CFStream (over internal TLS) + // + + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + CFIndex result = CFWriteStreamWrite(writeStream, buffer, (CFIndex)bytesToWrite); + LogVerbose(@"CFWriteStreamWrite(%lu) = %li", (unsigned long)bytesToWrite, result); + + if (result < 0) + { + error = (__bridge_transfer NSError *)CFWriteStreamCopyError(writeStream); + } + else + { + bytesWritten = (size_t)result; + + // We always set waiting to true in this scenario. + // CFStream may have altered our underlying socket to non-blocking. + // Thus if we attempt to write without a callback, we may end up blocking our queue. + waiting = YES; + } + + #endif + } + else + { + // We're going to use the SSLWrite function. + // + // OSStatus SSLWrite(SSLContextRef context, const void *data, size_t dataLength, size_t *processed) + // + // Parameters: + // context - An SSL session context reference. + // data - A pointer to the buffer of data to write. + // dataLength - The amount, in bytes, of data to write. + // processed - On return, the length, in bytes, of the data actually written. + // + // It sounds pretty straight-forward, + // but there are a few caveats you should be aware of. + // + // The SSLWrite method operates in a non-obvious (and rather annoying) manner. + // According to the documentation: + // + // Because you may configure the underlying connection to operate in a non-blocking manner, + // a write operation might return errSSLWouldBlock, indicating that less data than requested + // was actually transferred. In this case, you should repeat the call to SSLWrite until some + // other result is returned. + // + // This sounds perfect, but when our SSLWriteFunction returns errSSLWouldBlock, + // then the SSLWrite method returns (with the proper errSSLWouldBlock return value), + // but it sets processed to dataLength !! + // + // In other words, if the SSLWrite function doesn't completely write all the data we tell it to, + // then it doesn't tell us how many bytes were actually written. So, for example, if we tell it to + // write 256 bytes then it might actually write 128 bytes, but then report 0 bytes written. + // + // You might be wondering: + // If the SSLWrite function doesn't tell us how many bytes were written, + // then how in the world are we supposed to update our parameters (buffer & bytesToWrite) + // for the next time we invoke SSLWrite? + // + // The answer is that SSLWrite cached all the data we told it to write, + // and it will push out that data next time we call SSLWrite. + // If we call SSLWrite with new data, it will push out the cached data first, and then the new data. + // If we call SSLWrite with empty data, then it will simply push out the cached data. + // + // For this purpose we're going to break large writes into a series of smaller writes. + // This allows us to report progress back to the delegate. + + OSStatus result; + + BOOL hasCachedDataToWrite = (sslWriteCachedLength > 0); + BOOL hasNewDataToWrite = YES; + + if (hasCachedDataToWrite) + { + size_t processed = 0; + + result = SSLWrite(sslContext, NULL, 0, &processed); + + if (result == noErr) + { + bytesWritten = sslWriteCachedLength; + sslWriteCachedLength = 0; + + if ([currentWrite->buffer length] == (currentWrite->bytesDone + bytesWritten)) + { + // We've written all data for the current write. + hasNewDataToWrite = NO; + } + } + else + { + if (result == errSSLWouldBlock) + { + waiting = YES; + } + else + { + error = [self sslError:result]; + } + + // Can't write any new data since we were unable to write the cached data. + hasNewDataToWrite = NO; + } + } + + if (hasNewDataToWrite) + { + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + + currentWrite->bytesDone + + bytesWritten; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + size_t bytesRemaining = bytesToWrite; + + BOOL keepLooping = YES; + while (keepLooping) + { + const size_t sslMaxBytesToWrite = 32768; + size_t sslBytesToWrite = MIN(bytesRemaining, sslMaxBytesToWrite); + size_t sslBytesWritten = 0; + + result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten); + + if (result == noErr) + { + buffer += sslBytesWritten; + bytesWritten += sslBytesWritten; + bytesRemaining -= sslBytesWritten; + + keepLooping = (bytesRemaining > 0); + } + else + { + if (result == errSSLWouldBlock) + { + waiting = YES; + sslWriteCachedLength = sslBytesToWrite; + } + else + { + error = [self sslError:result]; + } + + keepLooping = NO; + } + + } // while (keepLooping) + + } // if (hasNewDataToWrite) + } + } + else + { + // + // Writing data directly over raw socket + // + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite); + LogVerbose(@"wrote to socket = %zd", result); + + // Check results + if (result < 0) + { + if (errno == EWOULDBLOCK) + { + waiting = YES; + } + else + { + error = [self errnoErrorWithReason:@"Error in write() function"]; + } + } + else + { + bytesWritten = result; + } + } + + // We're done with our writing. + // If we explictly ran into a situation where the socket told us there was no room in the buffer, + // then we immediately resume listening for notifications. + // + // We must do this before we dequeue another write, + // as that may in turn invoke this method again. + // + // Note that if CFStream is involved, it may have maliciously put our socket in blocking mode. + + if (waiting) + { + flags &= ~kSocketCanAcceptBytes; + + if (![self usingCFStreamForTLS]) + { + [self resumeWriteSource]; + } + } + + // Check our results + + BOOL done = NO; + + if (bytesWritten > 0) + { + // Update total amount read for the current write + currentWrite->bytesDone += bytesWritten; + LogVerbose(@"currentWrite->bytesDone = %lu", (unsigned long)currentWrite->bytesDone); + + // Is packet done? + done = (currentWrite->bytesDone == [currentWrite->buffer length]); + } + + if (done) + { + [self completeCurrentWrite]; + + if (!error) + { + dispatch_async(socketQueue, ^{ @autoreleasepool{ + + [self maybeDequeueWrite]; + }}); + } + } + else + { + // We were unable to finish writing the data, + // so we're waiting for another callback to notify us of available space in the lower-level output buffer. + + if (!waiting && !error) + { + // This would be the case if our write was able to accept some data, but not all of it. + + flags &= ~kSocketCanAcceptBytes; + + if (![self usingCFStreamForTLS]) + { + [self resumeWriteSource]; + } + } + + if (bytesWritten > 0) + { + // We're not done with the entire write, but we have written some bytes + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)]) + { + long theWriteTag = currentWrite->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag]; + }}); + } + } + } + + // Check for errors + + if (error) + { + [self closeWithError:[self errnoErrorWithReason:@"Error in write() function"]]; + } + + // Do not add any code here without first adding a return statement in the error case above. +} + +- (void)completeCurrentWrite +{ + LogTrace(); + + NSAssert(currentWrite, @"Trying to complete current write when there is no current write."); + + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWriteDataWithTag:)]) + { + long theWriteTag = currentWrite->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didWriteDataWithTag:theWriteTag]; + }}); + } + + [self endCurrentWrite]; +} + +- (void)endCurrentWrite +{ + if (writeTimer) + { + dispatch_source_cancel(writeTimer); + writeTimer = NULL; + } + + currentWrite = nil; +} + +- (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + writeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(writeTimer, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doWriteTimeout]; + + #pragma clang diagnostic pop + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theWriteTimer = writeTimer; + dispatch_source_set_cancel_handler(writeTimer, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(writeTimer)"); + dispatch_release(theWriteTimer); + + #pragma clang diagnostic pop + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(writeTimer); + } +} + +- (void)doWriteTimeout +{ + // This is a little bit tricky. + // Ideally we'd like to synchronously query the delegate about a timeout extension. + // But if we do so synchronously we risk a possible deadlock. + // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. + + flags |= kWritesPaused; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)]) + { + GCDAsyncWritePacket *theWrite = currentWrite; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + NSTimeInterval timeoutExtension = 0.0; + + timeoutExtension = [theDelegate socket:self shouldTimeoutWriteWithTag:theWrite->tag + elapsed:theWrite->timeout + bytesDone:theWrite->bytesDone]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self doWriteTimeoutWithExtension:timeoutExtension]; + }}); + }}); + } + else + { + [self doWriteTimeoutWithExtension:0.0]; + } +} + +- (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension +{ + if (currentWrite) + { + if (timeoutExtension > 0.0) + { + currentWrite->timeout += timeoutExtension; + + // Reschedule the timer + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); + + // Unpause writes, and continue + flags &= ~kWritesPaused; + [self doWriteData]; + } + else + { + LogVerbose(@"WriteTimeout"); + + [self closeWithError:[self writeTimeoutError]]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)startTLS:(NSDictionary *)tlsSettings +{ + LogTrace(); + + if (tlsSettings == nil) + { + // Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary, + // but causes problems if we later try to fetch the remote host's certificate. + // + // To be exact, it causes the following to return NULL instead of the normal result: + // CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates) + // + // So we use an empty dictionary instead, which works perfectly. + + tlsSettings = [NSDictionary dictionary]; + } + + GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if ((flags & kSocketStarted) && !(flags & kQueuedTLS) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [writeQueue addObject:packet]; + + flags |= kQueuedTLS; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } + }}); + +} + +- (void)maybeStartTLS +{ + // We can't start TLS until: + // - All queued reads prior to the user calling startTLS are complete + // - All queued writes prior to the user calling startTLS are complete + // + // We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + BOOL useSecureTransport = YES; + + #if TARGET_OS_IPHONE + { + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + NSDictionary *tlsSettings = @{}; + if (tlsPacket) { + tlsSettings = tlsPacket->tlsSettings; + } + NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS]; + if (value && [value boolValue]) + useSecureTransport = NO; + } + #endif + + if (useSecureTransport) + { + [self ssl_startTLS]; + } + else + { + #if TARGET_OS_IPHONE + [self cf_startTLS]; + #endif + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security via SecureTransport +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength +{ + LogVerbose(@"sslReadWithBuffer:%p length:%lu", buffer, (unsigned long)*bufferLength); + + if ((socketFDBytesAvailable == 0) && ([sslPreBuffer availableBytes] == 0)) + { + LogVerbose(@"%@ - No data available to read...", THIS_METHOD); + + // No data available to read. + // + // Need to wait for readSource to fire and notify us of + // available data in the socket's internal read buffer. + + [self resumeReadSource]; + + *bufferLength = 0; + return errSSLWouldBlock; + } + + size_t totalBytesRead = 0; + size_t totalBytesLeftToBeRead = *bufferLength; + + BOOL done = NO; + BOOL socketError = NO; + + // + // STEP 1 : READ FROM SSL PRE BUFFER + // + + size_t sslPreBufferLength = [sslPreBuffer availableBytes]; + + if (sslPreBufferLength > 0) + { + LogVerbose(@"%@: Reading from SSL pre buffer...", THIS_METHOD); + + size_t bytesToCopy; + if (sslPreBufferLength > totalBytesLeftToBeRead) + bytesToCopy = totalBytesLeftToBeRead; + else + bytesToCopy = sslPreBufferLength; + + LogVerbose(@"%@: Copying %zu bytes from sslPreBuffer", THIS_METHOD, bytesToCopy); + + memcpy(buffer, [sslPreBuffer readBuffer], bytesToCopy); + [sslPreBuffer didRead:bytesToCopy]; + + LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); + + totalBytesRead += bytesToCopy; + totalBytesLeftToBeRead -= bytesToCopy; + + done = (totalBytesLeftToBeRead == 0); + + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); + } + + // + // STEP 2 : READ FROM SOCKET + // + + if (!done && (socketFDBytesAvailable > 0)) + { + LogVerbose(@"%@: Reading from socket...", THIS_METHOD); + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + BOOL readIntoPreBuffer; + size_t bytesToRead; + uint8_t *buf; + + if (socketFDBytesAvailable > totalBytesLeftToBeRead) + { + // Read all available data from socket into sslPreBuffer. + // Then copy requested amount into dataBuffer. + + LogVerbose(@"%@: Reading into sslPreBuffer...", THIS_METHOD); + + [sslPreBuffer ensureCapacityForWrite:socketFDBytesAvailable]; + + readIntoPreBuffer = YES; + bytesToRead = (size_t)socketFDBytesAvailable; + buf = [sslPreBuffer writeBuffer]; + } + else + { + // Read available data from socket directly into dataBuffer. + + LogVerbose(@"%@: Reading directly into dataBuffer...", THIS_METHOD); + + readIntoPreBuffer = NO; + bytesToRead = totalBytesLeftToBeRead; + buf = (uint8_t *)buffer + totalBytesRead; + } + + ssize_t result = read(socketFD, buf, bytesToRead); + LogVerbose(@"%@: read from socket = %zd", THIS_METHOD, result); + + if (result < 0) + { + LogVerbose(@"%@: read errno = %i", THIS_METHOD, errno); + + if (errno != EWOULDBLOCK) + { + socketError = YES; + } + + socketFDBytesAvailable = 0; + } + else if (result == 0) + { + LogVerbose(@"%@: read EOF", THIS_METHOD); + + socketError = YES; + socketFDBytesAvailable = 0; + } + else + { + size_t bytesReadFromSocket = result; + + if (socketFDBytesAvailable > bytesReadFromSocket) + socketFDBytesAvailable -= bytesReadFromSocket; + else + socketFDBytesAvailable = 0; + + if (readIntoPreBuffer) + { + [sslPreBuffer didWrite:bytesReadFromSocket]; + + size_t bytesToCopy = MIN(totalBytesLeftToBeRead, bytesReadFromSocket); + + LogVerbose(@"%@: Copying %zu bytes out of sslPreBuffer", THIS_METHOD, bytesToCopy); + + memcpy((uint8_t *)buffer + totalBytesRead, [sslPreBuffer readBuffer], bytesToCopy); + [sslPreBuffer didRead:bytesToCopy]; + + totalBytesRead += bytesToCopy; + totalBytesLeftToBeRead -= bytesToCopy; + + LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); + } + else + { + totalBytesRead += bytesReadFromSocket; + totalBytesLeftToBeRead -= bytesReadFromSocket; + } + + done = (totalBytesLeftToBeRead == 0); + + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); + } + } + + *bufferLength = totalBytesRead; + + if (done) + return noErr; + + if (socketError) + return errSSLClosedAbort; + + return errSSLWouldBlock; +} + +- (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLength +{ + if (!(flags & kSocketCanAcceptBytes)) + { + // Unable to write. + // + // Need to wait for writeSource to fire and notify us of + // available space in the socket's internal write buffer. + + [self resumeWriteSource]; + + *bufferLength = 0; + return errSSLWouldBlock; + } + + size_t bytesToWrite = *bufferLength; + size_t bytesWritten = 0; + + BOOL done = NO; + BOOL socketError = NO; + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + ssize_t result = write(socketFD, buffer, bytesToWrite); + + if (result < 0) + { + if (errno != EWOULDBLOCK) + { + socketError = YES; + } + + flags &= ~kSocketCanAcceptBytes; + } + else if (result == 0) + { + flags &= ~kSocketCanAcceptBytes; + } + else + { + bytesWritten = result; + + done = (bytesWritten == bytesToWrite); + } + + *bufferLength = bytesWritten; + + if (done) + return noErr; + + if (socketError) + return errSSLClosedAbort; + + return errSSLWouldBlock; +} + +static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; + + NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); + + return [asyncSocket sslReadWithBuffer:data length:dataLength]; +} + +static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; + + NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); + + return [asyncSocket sslWriteWithBuffer:data length:dataLength]; +} + +- (void)ssl_startTLS +{ + LogTrace(); + + LogVerbose(@"Starting TLS (via SecureTransport)..."); + + OSStatus status; + + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + if (tlsPacket == nil) // Code to quiet the analyzer + { + NSAssert(NO, @"Logic error"); + + [self closeWithError:[self otherError:@"Logic error"]]; + return; + } + NSDictionary *tlsSettings = tlsPacket->tlsSettings; + + // Create SSLContext, and setup IO callbacks and connection ref + + BOOL isServer = [[tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLIsServer] boolValue]; + + #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) + { + if (isServer) + sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType); + else + sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType); + + if (sslContext == NULL) + { + [self closeWithError:[self otherError:@"Error in SSLCreateContext"]]; + return; + } + } + #else // (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) + { + status = SSLNewContext(isServer, &sslContext); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLNewContext"]]; + return; + } + } + #endif + + status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]]; + return; + } + + status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetConnection"]]; + return; + } + + + BOOL shouldManuallyEvaluateTrust = [[tlsSettings objectForKey:GCDAsyncSocketManuallyEvaluateTrust] boolValue]; + if (shouldManuallyEvaluateTrust) + { + if (isServer) + { + [self closeWithError:[self otherError:@"Manual trust validation is not supported for server sockets"]]; + return; + } + + status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetSessionOption"]]; + return; + } + + #if !TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) + + // Note from Apple's documentation: + // + // It is only necessary to call SSLSetEnableCertVerify on the Mac prior to OS X 10.8. + // On OS X 10.8 and later setting kSSLSessionOptionBreakOnServerAuth always disables the + // built-in trust evaluation. All versions of iOS behave like OS X 10.8 and thus + // SSLSetEnableCertVerify is not available on that platform at all. + + status = SSLSetEnableCertVerify(sslContext, NO); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]]; + return; + } + + #endif + } + + // Configure SSLContext from given settings + // + // Checklist: + // 1. kCFStreamSSLPeerName + // 2. kCFStreamSSLCertificates + // 3. GCDAsyncSocketSSLPeerID + // 4. GCDAsyncSocketSSLProtocolVersionMin + // 5. GCDAsyncSocketSSLProtocolVersionMax + // 6. GCDAsyncSocketSSLSessionOptionFalseStart + // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord + // 8. GCDAsyncSocketSSLCipherSuites + // 9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac) + // + // Deprecated (throw error): + // 10. kCFStreamSSLAllowsAnyRoot + // 11. kCFStreamSSLAllowsExpiredRoots + // 12. kCFStreamSSLAllowsExpiredCertificates + // 13. kCFStreamSSLValidatesCertificateChain + // 14. kCFStreamSSLLevel + + id value; + + // 1. kCFStreamSSLPeerName + + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLPeerName]; + if ([value isKindOfClass:[NSString class]]) + { + NSString *peerName = (NSString *)value; + + const char *peer = [peerName UTF8String]; + size_t peerLen = strlen(peer); + + status = SSLSetPeerDomainName(sslContext, peer, peerLen); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for kCFStreamSSLPeerName. Value must be of type NSString."); + + [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLPeerName."]]; + return; + } + + // 2. kCFStreamSSLCertificates + + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLCertificates]; + if ([value isKindOfClass:[NSArray class]]) + { + CFArrayRef certs = (__bridge CFArrayRef)value; + + status = SSLSetCertificate(sslContext, certs); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetCertificate"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for kCFStreamSSLCertificates. Value must be of type NSArray."); + + [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLCertificates."]]; + return; + } + + // 3. GCDAsyncSocketSSLPeerID + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLPeerID]; + if ([value isKindOfClass:[NSData class]]) + { + NSData *peerIdData = (NSData *)value; + + status = SSLSetPeerID(sslContext, [peerIdData bytes], [peerIdData length]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetPeerID"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLPeerID. Value must be of type NSData." + @" (You can convert strings to data using a method like" + @" [string dataUsingEncoding:NSUTF8StringEncoding])"); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLPeerID."]]; + return; + } + + // 4. GCDAsyncSocketSSLProtocolVersionMin + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMin]; + if ([value isKindOfClass:[NSNumber class]]) + { + SSLProtocol minProtocol = (SSLProtocol)[(NSNumber *)value intValue]; + if (minProtocol != kSSLProtocolUnknown) + { + status = SSLSetProtocolVersionMin(sslContext, minProtocol); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMin"]]; + return; + } + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMin. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMin."]]; + return; + } + + // 5. GCDAsyncSocketSSLProtocolVersionMax + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMax]; + if ([value isKindOfClass:[NSNumber class]]) + { + SSLProtocol maxProtocol = (SSLProtocol)[(NSNumber *)value intValue]; + if (maxProtocol != kSSLProtocolUnknown) + { + status = SSLSetProtocolVersionMax(sslContext, maxProtocol); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMax"]]; + return; + } + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMax. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMax."]]; + return; + } + + // 6. GCDAsyncSocketSSLSessionOptionFalseStart + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionFalseStart]; + if ([value isKindOfClass:[NSNumber class]]) + { + status = SSLSetSessionOption(sslContext, kSSLSessionOptionFalseStart, [value boolValue]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionFalseStart)"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart."]]; + return; + } + + // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionSendOneByteRecord]; + if ([value isKindOfClass:[NSNumber class]]) + { + status = SSLSetSessionOption(sslContext, kSSLSessionOptionSendOneByteRecord, [value boolValue]); + if (status != noErr) + { + [self closeWithError: + [self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionSendOneByteRecord)"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord." + @" Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord."]]; + return; + } + + // 8. GCDAsyncSocketSSLCipherSuites + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLCipherSuites]; + if ([value isKindOfClass:[NSArray class]]) + { + NSArray *cipherSuites = (NSArray *)value; + NSUInteger numberCiphers = [cipherSuites count]; + SSLCipherSuite ciphers[numberCiphers]; + + NSUInteger cipherIndex; + for (cipherIndex = 0; cipherIndex < numberCiphers; cipherIndex++) + { + NSNumber *cipherObject = [cipherSuites objectAtIndex:cipherIndex]; + ciphers[cipherIndex] = [cipherObject shortValue]; + } + + status = SSLSetEnabledCiphers(sslContext, ciphers, numberCiphers); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetEnabledCiphers"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLCipherSuites. Value must be of type NSArray."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLCipherSuites."]]; + return; + } + + // 9. GCDAsyncSocketSSLDiffieHellmanParameters + + #if !TARGET_OS_IPHONE + value = [tlsSettings objectForKey:GCDAsyncSocketSSLDiffieHellmanParameters]; + if ([value isKindOfClass:[NSData class]]) + { + NSData *diffieHellmanData = (NSData *)value; + + status = SSLSetDiffieHellmanParams(sslContext, [diffieHellmanData bytes], [diffieHellmanData length]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetDiffieHellmanParams"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters. Value must be of type NSData."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters."]]; + return; + } + #endif + + // DEPRECATED checks + + // 10. kCFStreamSSLAllowsAnyRoot + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsAnyRoot]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsAnyRoot" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsAnyRoot"]]; + return; + } + + // 11. kCFStreamSSLAllowsExpiredRoots + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredRoots]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredRoots" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredRoots"]]; + return; + } + + // 12. kCFStreamSSLValidatesCertificateChain + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLValidatesCertificateChain]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLValidatesCertificateChain" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLValidatesCertificateChain"]]; + return; + } + + // 13. kCFStreamSSLAllowsExpiredCertificates + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredCertificates]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates"]]; + return; + } + + // 14. kCFStreamSSLLevel + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLLevel]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLLevel" + @" - You must use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMax"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLLevel"]]; + return; + } + + // Setup the sslPreBuffer + // + // Any data in the preBuffer needs to be moved into the sslPreBuffer, + // as this data is now part of the secure read stream. + + sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; + + size_t preBufferLength = [preBuffer availableBytes]; + + if (preBufferLength > 0) + { + [sslPreBuffer ensureCapacityForWrite:preBufferLength]; + + memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength); + [preBuffer didRead:preBufferLength]; + [sslPreBuffer didWrite:preBufferLength]; + } + + sslErrCode = lastSSLHandshakeError = noErr; + + // Start the SSL Handshake process + + [self ssl_continueSSLHandshake]; +} + +- (void)ssl_continueSSLHandshake +{ + LogTrace(); + + // If the return value is noErr, the session is ready for normal secure communication. + // If the return value is errSSLWouldBlock, the SSLHandshake function must be called again. + // If the return value is errSSLServerAuthCompleted, we ask delegate if we should trust the + // server and then call SSLHandshake again to resume the handshake or close the connection + // errSSLPeerBadCert SSL error. + // Otherwise, the return value indicates an error code. + + OSStatus status = SSLHandshake(sslContext); + lastSSLHandshakeError = status; + + if (status == noErr) + { + LogVerbose(@"SSLHandshake complete"); + + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + flags |= kSocketSecure; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidSecure:self]; + }}); + } + + [self endCurrentRead]; + [self endCurrentWrite]; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } + else if (status == errSSLPeerAuthCompleted) + { + LogVerbose(@"SSLHandshake peerAuthCompleted - awaiting delegate approval"); + + __block SecTrustRef trust = NULL; + status = SSLCopyPeerTrust(sslContext, &trust); + if (status != noErr) + { + [self closeWithError:[self sslError:status]]; + return; + } + + int aStateIndex = stateIndex; + dispatch_queue_t theSocketQueue = socketQueue; + + __weak GCDAsyncSocket *weakSelf = self; + + void (^comletionHandler)(BOOL) = ^(BOOL shouldTrust){ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + dispatch_async(theSocketQueue, ^{ @autoreleasepool { + + if (trust) { + CFRelease(trust); + trust = NULL; + } + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf) + { + [strongSelf ssl_shouldTrustPeer:shouldTrust stateIndex:aStateIndex]; + } + }}); + + #pragma clang diagnostic pop + }}; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReceiveTrust:completionHandler:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler]; + }}); + } + else + { + if (trust) { + CFRelease(trust); + trust = NULL; + } + + NSString *msg = @"GCDAsyncSocketManuallyEvaluateTrust specified in tlsSettings," + @" but delegate doesn't implement socket:shouldTrustPeer:"; + + [self closeWithError:[self otherError:msg]]; + return; + } + } + else if (status == errSSLWouldBlock) + { + LogVerbose(@"SSLHandshake continues..."); + + // Handshake continues... + // + // This method will be called again from doReadData or doWriteData. + } + else + { + [self closeWithError:[self sslError:status]]; + } +} + +- (void)ssl_shouldTrustPeer:(BOOL)shouldTrust stateIndex:(int)aStateIndex +{ + LogTrace(); + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring ssl_shouldTrustPeer - invalid state (maybe disconnected)"); + + // One of the following is true + // - the socket was disconnected + // - the startTLS operation timed out + // - the completionHandler was already invoked once + + return; + } + + // Increment stateIndex to ensure completionHandler can only be called once. + stateIndex++; + + if (shouldTrust) + { + NSAssert(lastSSLHandshakeError == errSSLPeerAuthCompleted, @"ssl_shouldTrustPeer called when last error is %d and not errSSLPeerAuthCompleted", (int)lastSSLHandshakeError); + [self ssl_continueSSLHandshake]; + } + else + { + [self closeWithError:[self sslError:errSSLPeerBadCert]]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security via CFStream +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + +- (void)cf_finishSSLHandshake +{ + LogTrace(); + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + flags |= kSocketSecure; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidSecure:self]; + }}); + } + + [self endCurrentRead]; + [self endCurrentWrite]; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } +} + +- (void)cf_abortSSLHandshake:(NSError *)error +{ + LogTrace(); + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + [self closeWithError:error]; + } +} + +- (void)cf_startTLS +{ + LogTrace(); + + LogVerbose(@"Starting TLS (via CFStream)..."); + + if ([preBuffer availableBytes] > 0) + { + NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + [self suspendReadSource]; + [self suspendWriteSource]; + + socketFDBytesAvailable = 0; + flags &= ~kSocketCanAcceptBytes; + flags &= ~kSecureSocketHasBytesAvailable; + + flags |= kUsingCFStreamForTLS; + + if (![self createReadAndWriteStream]) + { + [self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]]; + return; + } + + if (![self registerForStreamCallbacksIncludingReadWrite:YES]) + { + [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; + return; + } + + if (![self addStreamsToRunLoop]) + { + [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; + return; + } + + NSAssert([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid read packet for startTLS"); + NSAssert([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid write packet for startTLS"); + + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings; + + // Getting an error concerning kCFStreamPropertySSLSettings ? + // You need to add the CFNetwork framework to your iOS application. + + BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings); + BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings); + + // For some reason, starting around the time of iOS 4.3, + // the first call to set the kCFStreamPropertySSLSettings will return true, + // but the second will return false. + // + // Order doesn't seem to matter. + // So you could call CFReadStreamSetProperty and then CFWriteStreamSetProperty, or you could reverse the order. + // Either way, the first call will return true, and the second returns false. + // + // Interestingly, this doesn't seem to affect anything. + // Which is not altogether unusual, as the documentation seems to suggest that (for many settings) + // setting it on one side of the stream automatically sets it for the other side of the stream. + // + // Although there isn't anything in the documentation to suggest that the second attempt would fail. + // + // Furthermore, this only seems to affect streams that are negotiating a security upgrade. + // In other words, the socket gets connected, there is some back-and-forth communication over the unsecure + // connection, and then a startTLS is issued. + // So this mostly affects newer protocols (XMPP, IMAP) as opposed to older protocols (HTTPS). + + if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug. + { + [self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]]; + return; + } + + if (![self openStreams]) + { + [self closeWithError:[self otherError:@"Error in CFStreamOpen"]]; + return; + } + + LogVerbose(@"Waiting for SSL Handshake to complete..."); +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark CFStream +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + ++ (void)ignore:(id)_ +{} + ++ (void)startCFStreamThreadIfNeeded +{ + LogTrace(); + + static dispatch_once_t predicate; + dispatch_once(&predicate, ^{ + + cfstreamThreadRetainCount = 0; + cfstreamThreadSetupQueue = dispatch_queue_create("GCDAsyncSocket-CFStreamThreadSetup", DISPATCH_QUEUE_SERIAL); + }); + + dispatch_sync(cfstreamThreadSetupQueue, ^{ @autoreleasepool { + + if (++cfstreamThreadRetainCount == 1) + { + cfstreamThread = [[NSThread alloc] initWithTarget:self + selector:@selector(cfstreamThread) + object:nil]; + [cfstreamThread start]; + } + }}); +} + ++ (void)stopCFStreamThreadIfNeeded +{ + LogTrace(); + + // The creation of the cfstreamThread is relatively expensive. + // So we'd like to keep it available for recycling. + // However, there's a tradeoff here, because it shouldn't remain alive forever. + // So what we're going to do is use a little delay before taking it down. + // This way it can be reused properly in situations where multiple sockets are continually in flux. + + int delayInSeconds = 30; + dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(when, cfstreamThreadSetupQueue, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + if (cfstreamThreadRetainCount == 0) + { + LogWarn(@"Logic error concerning cfstreamThread start / stop"); + return_from_block; + } + + if (--cfstreamThreadRetainCount == 0) + { + [cfstreamThread cancel]; // set isCancelled flag + + // wake up the thread + [[self class] performSelector:@selector(ignore:) + onThread:cfstreamThread + withObject:[NSNull null] + waitUntilDone:NO]; + + cfstreamThread = nil; + } + + #pragma clang diagnostic pop + }}); +} + ++ (void)cfstreamThread { @autoreleasepool +{ + [[NSThread currentThread] setName:GCDAsyncSocketThreadName]; + + LogInfo(@"CFStreamThread: Started"); + + // We can't run the run loop unless it has an associated input source or a timer. + // So we'll just create a timer that will never fire - unless the server runs for decades. + [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] + target:self + selector:@selector(ignore:) + userInfo:nil + repeats:YES]; + + NSThread *currentThread = [NSThread currentThread]; + NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop]; + + BOOL isCancelled = [currentThread isCancelled]; + + while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) + { + isCancelled = [currentThread isCancelled]; + } + + LogInfo(@"CFStreamThread: Stopped"); +}} + ++ (void)scheduleCFStreams:(GCDAsyncSocket *)asyncSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncSocket->readStream) + CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); + + if (asyncSocket->writeStream) + CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); +} + ++ (void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncSocket->readStream) + CFReadStreamUnscheduleFromRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); + + if (asyncSocket->writeStream) + CFWriteStreamUnscheduleFromRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); +} + +static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; + + switch(type) + { + case kCFStreamEventHasBytesAvailable: + { + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); + + if (asyncSocket->readStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. + // (A callback related to the tcp stream, but not to the SSL layer). + + if (CFReadStreamHasBytesAvailable(asyncSocket->readStream)) + { + asyncSocket->flags |= kSecureSocketHasBytesAvailable; + [asyncSocket cf_finishSSLHandshake]; + } + } + else + { + asyncSocket->flags |= kSecureSocketHasBytesAvailable; + [asyncSocket doReadData]; + } + }}); + + break; + } + default: + { + NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); + + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncSocket connectionClosedError]; + } + + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - Other"); + + if (asyncSocket->readStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + [asyncSocket cf_abortSSLHandshake:error]; + } + else + { + [asyncSocket closeWithError:error]; + } + }}); + + break; + } + } + +} + +static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; + + switch(type) + { + case kCFStreamEventCanAcceptBytes: + { + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); + + if (asyncSocket->writeStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. + // (A callback related to the tcp stream, but not to the SSL layer). + + if (CFWriteStreamCanAcceptBytes(asyncSocket->writeStream)) + { + asyncSocket->flags |= kSocketCanAcceptBytes; + [asyncSocket cf_finishSSLHandshake]; + } + } + else + { + asyncSocket->flags |= kSocketCanAcceptBytes; + [asyncSocket doWriteData]; + } + }}); + + break; + } + default: + { + NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); + + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncSocket connectionClosedError]; + } + + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - Other"); + + if (asyncSocket->writeStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + [asyncSocket cf_abortSSLHandshake:error]; + } + else + { + [asyncSocket closeWithError:error]; + } + }}); + + break; + } + } + +} + +- (BOOL)createReadAndWriteStream +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (readStream || writeStream) + { + // Streams already created + return YES; + } + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + if (socketFD == SOCKET_NULL) + { + // Cannot create streams without a file descriptor + return NO; + } + + if (![self isConnected]) + { + // Cannot create streams until file descriptor is connected + return NO; + } + + LogVerbose(@"Creating read and write stream..."); + + CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream); + + // The kCFStreamPropertyShouldCloseNativeSocket property should be false by default (for our case). + // But let's not take any chances. + + if (readStream) + CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + if (writeStream) + CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + + if ((readStream == NULL) || (writeStream == NULL)) + { + LogWarn(@"Unable to create read and write stream..."); + + if (readStream) + { + CFReadStreamClose(readStream); + CFRelease(readStream); + readStream = NULL; + } + if (writeStream) + { + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + writeStream = NULL; + } + + return NO; + } + + return YES; +} + +- (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite +{ + LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO")); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + streamContext.version = 0; + streamContext.info = (__bridge void *)(self); + streamContext.retain = nil; + streamContext.release = nil; + streamContext.copyDescription = nil; + + CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (includeReadWrite) + readStreamEvents |= kCFStreamEventHasBytesAvailable; + + if (!CFReadStreamSetClient(readStream, readStreamEvents, &CFReadStreamCallback, &streamContext)) + { + return NO; + } + + CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (includeReadWrite) + writeStreamEvents |= kCFStreamEventCanAcceptBytes; + + if (!CFWriteStreamSetClient(writeStream, writeStreamEvents, &CFWriteStreamCallback, &streamContext)) + { + return NO; + } + + return YES; +} + +- (BOOL)addStreamsToRunLoop +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + if (!(flags & kAddedStreamsToRunLoop)) + { + LogVerbose(@"Adding streams to runloop..."); + + [[self class] startCFStreamThreadIfNeeded]; + [[self class] performSelector:@selector(scheduleCFStreams:) + onThread:cfstreamThread + withObject:self + waitUntilDone:YES]; + + flags |= kAddedStreamsToRunLoop; + } + + return YES; +} + +- (void)removeStreamsFromRunLoop +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + if (flags & kAddedStreamsToRunLoop) + { + LogVerbose(@"Removing streams from runloop..."); + + [[self class] performSelector:@selector(unscheduleCFStreams:) + onThread:cfstreamThread + withObject:self + waitUntilDone:YES]; + [[self class] stopCFStreamThreadIfNeeded]; + + flags &= ~kAddedStreamsToRunLoop; + } +} + +- (BOOL)openStreams +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + CFStreamStatus readStatus = CFReadStreamGetStatus(readStream); + CFStreamStatus writeStatus = CFWriteStreamGetStatus(writeStream); + + if ((readStatus == kCFStreamStatusNotOpen) || (writeStatus == kCFStreamStatusNotOpen)) + { + LogVerbose(@"Opening read and write stream..."); + + BOOL r1 = CFReadStreamOpen(readStream); + BOOL r2 = CFWriteStreamOpen(writeStream); + + if (!r1 || !r2) + { + LogError(@"Error in CFStreamOpen"); + return NO; + } + } + + return YES; +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Advanced +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * See header file for big discussion of this method. +**/ +- (BOOL)autoDisconnectOnClosedReadStream +{ + // Note: YES means kAllowHalfDuplexConnection is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kAllowHalfDuplexConnection) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kAllowHalfDuplexConnection) == 0); + }); + + return result; + } +} + +/** + * See header file for big discussion of this method. +**/ +- (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag +{ + // Note: YES means kAllowHalfDuplexConnection is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kAllowHalfDuplexConnection; + else + config |= kAllowHalfDuplexConnection; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + + +/** + * See header file for big discussion of this method. +**/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue +{ + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); +} + +/** + * See header file for big discussion of this method. +**/ +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue +{ + dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL); +} + +/** + * See header file for big discussion of this method. +**/ +- (void)performBlock:(dispatch_block_t)block +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socketFD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + if (socket4FD != SOCKET_NULL) + return socket4FD; + else + return socket6FD; +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socket4FD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + return socket4FD; +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socket6FD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + return socket6FD; +} + +#if TARGET_OS_IPHONE + +/** + * Questions? Have you read the header file? +**/ +- (CFReadStreamRef)readStream +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + if (readStream == NULL) + [self createReadAndWriteStream]; + + return readStream; +} + +/** + * Questions? Have you read the header file? +**/ +- (CFWriteStreamRef)writeStream +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + if (writeStream == NULL) + [self createReadAndWriteStream]; + + return writeStream; +} + +- (BOOL)enableBackgroundingOnSocketWithCaveat:(BOOL)caveat +{ + if (![self createReadAndWriteStream]) + { + // Error occurred creating streams (perhaps socket isn't open) + return NO; + } + + BOOL r1, r2; + + LogVerbose(@"Enabling backgrouding on socket"); + + r1 = CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + r2 = CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + + if (!r1 || !r2) + { + return NO; + } + + if (!caveat) + { + if (![self openStreams]) + { + return NO; + } + } + + return YES; +} + +/** + * Questions? Have you read the header file? +**/ +- (BOOL)enableBackgroundingOnSocket +{ + LogTrace(); + + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NO; + } + + return [self enableBackgroundingOnSocketWithCaveat:NO]; +} + +- (BOOL)enableBackgroundingOnSocketWithCaveat // Deprecated in iOS 4.??? +{ + // This method was created as a workaround for a bug in iOS. + // Apple has since fixed this bug. + // I'm not entirely sure which version of iOS they fixed it in... + + LogTrace(); + + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NO; + } + + return [self enableBackgroundingOnSocketWithCaveat:YES]; +} + +#endif + +- (SSLContextRef)sslContext +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + return sslContext; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Class Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr +{ + LogTrace(); + + NSMutableArray *addresses = nil; + NSError *error = nil; + + if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) + { + // Use LOOPBACK address + struct sockaddr_in nativeAddr4; + nativeAddr4.sin_len = sizeof(struct sockaddr_in); + nativeAddr4.sin_family = AF_INET; + nativeAddr4.sin_port = htons(port); + nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); + + struct sockaddr_in6 nativeAddr6; + nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); + nativeAddr6.sin6_family = AF_INET6; + nativeAddr6.sin6_port = htons(port); + nativeAddr6.sin6_flowinfo = 0; + nativeAddr6.sin6_addr = in6addr_loopback; + nativeAddr6.sin6_scope_id = 0; + + // Wrap the native address structures + + NSData *address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + NSData *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + + addresses = [NSMutableArray arrayWithCapacity:2]; + [addresses addObject:address4]; + [addresses addObject:address6]; + } + else + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); + + if (gai_error) + { + error = [self gaiError:gai_error]; + } + else + { + NSUInteger capacity = 0; + for (res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET || res->ai_family == AF_INET6) { + capacity++; + } + } + + addresses = [NSMutableArray arrayWithCapacity:capacity]; + + for (res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET) + { + // Found IPv4 address. + // Wrap the native address structure, and add to results. + + NSData *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + [addresses addObject:address4]; + } + else if (res->ai_family == AF_INET6) + { + // Fixes connection issues with IPv6 + // https://github.com/robbiehanson/CocoaAsyncSocket/issues/429#issuecomment-222477158 + + // Found IPv6 address. + // Wrap the native address structure, and add to results. + + struct sockaddr_in6 *sockaddr = (struct sockaddr_in6 *)res->ai_addr; + in_port_t *portPtr = &sockaddr->sin6_port; + if ((portPtr != NULL) && (*portPtr == 0)) { + *portPtr = htons(port); + } + + NSData *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + [addresses addObject:address6]; + } + } + freeaddrinfo(res0); + + if ([addresses count] == 0) + { + error = [self gaiError:EAI_FAIL]; + } + } + } + + if (errPtr) *errPtr = error; + return addresses; +} + ++ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + char addrBuf[INET_ADDRSTRLEN]; + + if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + char addrBuf[INET6_ADDRSTRLEN]; + + if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + return ntohs(pSockaddr4->sin_port); +} + ++ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + return ntohs(pSockaddr6->sin6_port); +} + ++ (NSURL *)urlFromSockaddrUN:(const struct sockaddr_un *)pSockaddr +{ + NSString *path = [NSString stringWithUTF8String:pSockaddr->sun_path]; + return [NSURL fileURLWithPath:path]; +} + ++ (NSString *)hostFromAddress:(NSData *)address +{ + NSString *host; + + if ([self getHost:&host port:NULL fromAddress:address]) + return host; + else + return nil; +} + ++ (uint16_t)portFromAddress:(NSData *)address +{ + uint16_t port; + + if ([self getHost:NULL port:&port fromAddress:address]) + return port; + else + return 0; +} + ++ (BOOL)isIPv4Address:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET) { + return YES; + } + } + + return NO; +} + ++ (BOOL)isIPv6Address:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET6) { + return YES; + } + } + + return NO; +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address +{ + return [self getHost:hostPtr port:portPtr family:NULL fromAddress:address]; +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(sa_family_t *)afPtr fromAddress:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET) + { + if ([address length] >= sizeof(struct sockaddr_in)) + { + struct sockaddr_in sockaddr4; + memcpy(&sockaddr4, sockaddrX, sizeof(sockaddr4)); + + if (hostPtr) *hostPtr = [self hostFromSockaddr4:&sockaddr4]; + if (portPtr) *portPtr = [self portFromSockaddr4:&sockaddr4]; + if (afPtr) *afPtr = AF_INET; + + return YES; + } + } + else if (sockaddrX->sa_family == AF_INET6) + { + if ([address length] >= sizeof(struct sockaddr_in6)) + { + struct sockaddr_in6 sockaddr6; + memcpy(&sockaddr6, sockaddrX, sizeof(sockaddr6)); + + if (hostPtr) *hostPtr = [self hostFromSockaddr6:&sockaddr6]; + if (portPtr) *portPtr = [self portFromSockaddr6:&sockaddr6]; + if (afPtr) *afPtr = AF_INET6; + + return YES; + } + } + } + + return NO; +} + ++ (NSData *)CRLFData +{ + return [NSData dataWithBytes:"\x0D\x0A" length:2]; +} + ++ (NSData *)CRData +{ + return [NSData dataWithBytes:"\x0D" length:1]; +} + ++ (NSData *)LFData +{ + return [NSData dataWithBytes:"\x0A" length:1]; +} + ++ (NSData *)ZeroData +{ + return [NSData dataWithBytes:"" length:1]; +} + +@end diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/AADevice.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/AADevice.swift index 5e20b9e8a8..31571806a7 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/AADevice.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/AADevice.swift @@ -10,20 +10,20 @@ public struct AADevice { // // Device Types // - public static let isiPad = UIDevice.currentDevice().userInterfaceIdiom == .Pad - public static let isiPhone = UIDevice.currentDevice().userInterfaceIdiom == .Phone + public static let isiPad = UIDevice.current.userInterfaceIdiom == .pad + public static let isiPhone = UIDevice.current.userInterfaceIdiom == .phone // // OS Versions // public static let isiOS8 = true - public static let isiOS9 = NSProcessInfo.processInfo().isOperatingSystemAtLeastVersion( NSOperatingSystemVersion(majorVersion: 9, minorVersion: 0, patchVersion: 0)) + public static let isiOS9 = ProcessInfo.processInfo.isOperatingSystemAtLeast( OperatingSystemVersion(majorVersion: 9, minorVersion: 0, patchVersion: 0)) // // Device Sizes // - public static let screenWidth = min(UIScreen.mainScreen().bounds.size.width, UIScreen.mainScreen().bounds.size.height) - public static let screenHeight = max(UIScreen.mainScreen().bounds.size.width, UIScreen.mainScreen().bounds.size.height) + public static let screenWidth = min(UIScreen.main.bounds.size.width, UIScreen.main.bounds.size.height) + public static let screenHeight = max(UIScreen.main.bounds.size.width, UIScreen.main.bounds.size.height) // // iPhone sizes @@ -32,4 +32,4 @@ public struct AADevice { public static let isiPhone5 = isiPhone && screenHeight == 568.0 public static let isiPhone6 = isiPhone && screenHeight == 667.0 public static let isiPhone6P = isiPhone && screenHeight == 736.0 -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/AALocalized.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/AALocalized.swift index 34dd55f330..2402359ba8 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/AALocalized.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/AALocalized.swift @@ -6,10 +6,7 @@ import Foundation // Shorter helper for localized strings -public func AALocalized(text: String!) -> String! { - if text == nil { - return nil - } +public func AALocalized(_ text: String) -> String { let appRes = NSLocalizedString(text, comment: "") @@ -24,12 +21,12 @@ public func AALocalized(text: String!) -> String! { } } - return NSLocalizedString(text, tableName: nil, bundle: NSBundle.framework, value: text, comment: "") + return NSLocalizedString(text, tableName: nil, bundle: Bundle.framework, value: text, comment: "") } // Registration localization table -public func AARegisterLocalizedBundle(table: String, bundle: NSBundle) { +public func AARegisterLocalizedBundle(_ table: String, bundle: Bundle) { tables.append(LocTable(table: table, bundle: bundle)) } @@ -38,9 +35,9 @@ private var tables = [LocTable]() private class LocTable { let table: String - let bundle: NSBundle + let bundle: Bundle - init(table: String, bundle: NSBundle) { + init(table: String, bundle: Bundle) { self.table = table self.bundle = bundle } @@ -64,4 +61,4 @@ public extension UILabel { } } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/AARegex.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/AARegex.swift index fe7ba087ff..23443bccd1 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/AARegex.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/AARegex.swift @@ -12,14 +12,14 @@ class AARegex { init(_ pattern: String) { self.pattern = pattern do { - self.internalExpression = try NSRegularExpression(pattern: pattern, options: .CaseInsensitive) + self.internalExpression = try NSRegularExpression(pattern: pattern, options: .caseInsensitive) } catch { fatalError("Incorrect regex: \(pattern)") } } - func test(input: String) -> Bool { - let matches = self.internalExpression.matchesInString(input, options: NSMatchingOptions(), range:NSMakeRange(0, input.length)) + func test(_ input: String) -> Bool { + let matches = self.internalExpression.matches(in: input, options: NSRegularExpression.MatchingOptions(), range:NSMakeRange(0, input.length)) return matches.count > 0 } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/AssosiatedObject.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/AssosiatedObject.swift index 2d31e8ce21..a4eaf78f9b 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/AssosiatedObject.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/AssosiatedObject.swift @@ -5,7 +5,7 @@ import Foundation import ObjectiveC -public func setAssociatedObject(object: AnyObject, value: T, associativeKey: UnsafePointer, policy: objc_AssociationPolicy = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) { +public func setAssociatedObject(_ object: AnyObject, value: T, associativeKey: UnsafeRawPointer, policy: objc_AssociationPolicy = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) { if let v: AnyObject = value as? AnyObject { objc_setAssociatedObject(object, associativeKey, v, policy) } @@ -14,7 +14,7 @@ public func setAssociatedObject(object: AnyObject, value: T, associativeKey: } } -public func getAssociatedObject(object: AnyObject, associativeKey: UnsafePointer) -> T? { +public func getAssociatedObject(_ object: AnyObject, associativeKey: UnsafeRawPointer) -> T? { if let v = objc_getAssociatedObject(object, associativeKey) as? T { return v } @@ -33,6 +33,6 @@ final class Lifted { } } -private func lift(x: T) -> Lifted { +private func lift(_ x: T) -> Lifted { return Lifted(x) -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Collections.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Collections.swift index 934286db48..5792e7bda6 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Collections.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Collections.swift @@ -5,7 +5,7 @@ import Foundation public extension Array { - public func contains(obj: T) -> Bool { + public func contains(_ obj: T) -> Bool where T : Equatable { return self.filter({$0 as? T == obj}).count > 0 } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Colors.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Colors.swift index 11ad585987..981c8dae6c 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Colors.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Colors.swift @@ -6,12 +6,12 @@ import UIKit public extension UIColor { - public convenience init(rgb: UInt) { - self.init(red: CGFloat((rgb & 0xFF0000) >> 16) / 255.0, - green: CGFloat((rgb & 0x00FF00) >> 8) / 255.0, - blue: CGFloat(rgb & 0x0000FF) / 255.0, - alpha: CGFloat(1.0)) - } +// public convenience init(rgb: UInt) { +// self.init(red: CGFloat((rgb & 0xFF0000) >> 16) / 255.0, +// green: CGFloat((rgb & 0x00FF00) >> 8) / 255.0, +// blue: CGFloat(rgb & 0x0000FF) / 255.0, +// alpha: CGFloat(1.0)) +// } public convenience init(rgb: UInt, alpha: Double) { self.init(red: CGFloat((rgb & 0xFF0000) >> 16) / 255.0, @@ -27,15 +27,15 @@ public extension UIColor { alpha: CGFloat(1.0)) } - public class func alphaBlack(alpha: Double) -> UIColor { + public class func alphaBlack(_ alpha: Double) -> UIColor { return UIColor(red: 0, green: 0, blue: 0, alpha: CGFloat(alpha)) } - public class func alphaWhite(alpha: Double) -> UIColor { + public class func alphaWhite(_ alpha: Double) -> UIColor { return UIColor(red: 1, green: 1, blue: 1, alpha: CGFloat(alpha)) } - public func alpha(alpha: Double) -> UIColor { + public func alpha(_ alpha: Double) -> UIColor { var r:CGFloat = 0 var g:CGFloat = 0 var b:CGFloat = 0 diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Dispatch.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Dispatch.swift index 59f3a65b8a..d005943fbc 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Dispatch.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Dispatch.swift @@ -4,36 +4,36 @@ import Foundation -private let backgroundQueue = dispatch_queue_create("im.actor.background", DISPATCH_QUEUE_SERIAL) +private let backgroundQueue = DispatchQueue(label: "im.actor.background", attributes: []) -public func dispatchOnUi(closure: () -> Void) { - dispatch_async(dispatch_get_main_queue(), { () -> Void in +public func dispatchOnUi(_ closure: @escaping () -> Void) { + DispatchQueue.main.async(execute: { () -> Void in closure() }) } -public func dispatchOnUiSync(closure: () -> Void) { - dispatch_sync(dispatch_get_main_queue(), { () -> Void in +public func dispatchOnUiSync(_ closure: () -> Void) { + DispatchQueue.main.sync(execute: { () -> Void in closure() }) } -public func dispatchAfterOnUi(delay: Double, closure: () -> Void) { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { () -> Void in +public func dispatchAfterOnUi(_ delay: Double, closure: @escaping () -> Void) { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { () -> Void in closure() } } -public func dispatchBackground(closure: () -> Void) { - dispatch_async(backgroundQueue) { +public func dispatchBackground(_ closure: @escaping () -> Void) { + backgroundQueue.async { closure() } } -public func dispatchBackgroundDelayed(delay: Double, closure: () -> Void) { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), backgroundQueue) { +public func dispatchBackgroundDelayed(_ delay: Double, closure: @escaping () -> Void) { + backgroundQueue.asyncAfter(deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { closure() } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Fonts.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Fonts.swift index d5874755df..669eaf7ae7 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Fonts.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Fonts.swift @@ -6,25 +6,25 @@ import Foundation public extension UIFont { - public class func thinSystemFontOfSize(size: CGFloat) -> UIFont { + public class func thinSystemFontOfSize(_ size: CGFloat) -> UIFont { if #available(iOS 8.2, *) { - return UIFont.systemFontOfSize(size, weight: UIFontWeightThin) + return UIFont.systemFont(ofSize: size, weight: UIFontWeightThin) } else { return UIFont(name: "HelveticaNeue-Thin", size: size)! } } - public class func lightSystemFontOfSize(size: CGFloat) -> UIFont { + public class func lightSystemFontOfSize(_ size: CGFloat) -> UIFont { if #available(iOS 8.2, *) { - return UIFont.systemFontOfSize(size, weight: UIFontWeightLight) + return UIFont.systemFont(ofSize: size, weight: UIFontWeightLight) } else { return UIFont(name: "HelveticaNeue-Light", size: size)! } } - public class func mediumSystemFontOfSize(size: CGFloat) -> UIFont { + public class func mediumSystemFontOfSize(_ size: CGFloat) -> UIFont { if #available(iOS 8.2, *) { - return UIFont.systemFontOfSize(size, weight: UIFontWeightMedium) + return UIFont.systemFont(ofSize: size, weight: UIFontWeightMedium) } else { return UIFont(name: "HelveticaNeue-Medium", size: size)! } @@ -32,15 +32,15 @@ public extension UIFont { // Texts - public class func textFontOfSize(size: CGFloat) -> UIFont { + public class func textFontOfSize(_ size: CGFloat) -> UIFont { return UIFont(name: "HelveticaNeue", size: size)! } - public class func italicTextFontOfSize(size: CGFloat) -> UIFont { + public class func italicTextFontOfSize(_ size: CGFloat) -> UIFont { return UIFont(name: "HelveticaNeue-Italic", size: size)! } - public class func boldTextFontOfSize(size: CGFloat) -> UIFont { + public class func boldTextFontOfSize(_ size: CGFloat) -> UIFont { return UIFont(name: "HelveticaNeue-Medium", size: size)! } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Images.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Images.swift index 757b4bb963..ed9ca3e8fc 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Images.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Images.swift @@ -9,18 +9,18 @@ import Accelerate public extension UIImage { - class func tinted(named: String, color: UIColor) -> UIImage { + class func tinted(_ named: String, color: UIColor) -> UIImage { return UIImage.bundled(named)!.tintImage(color) } - class func templated(named: String) -> UIImage { - return UIImage.bundled(named)!.imageWithRenderingMode(.AlwaysTemplate) + class func templated(_ named: String) -> UIImage { + return UIImage.bundled(named)!.withRenderingMode(.alwaysTemplate) } - public func tintImage(color:UIColor) -> UIImage{ - UIGraphicsBeginImageContextWithOptions(self.size,false,UIScreen.mainScreen().scale); + public func tintImage(_ color:UIColor) -> UIImage{ + UIGraphicsBeginImageContextWithOptions(self.size,false,UIScreen.main.scale); - var rect = CGRectZero; + var rect = CGRect.zero; rect.size = self.size; // Composite tint color at its own opacity. color.set(); @@ -28,22 +28,22 @@ public extension UIImage { // Mask tint color-swatch to this image's opaque mask. // We want behaviour like NSCompositeDestinationIn on Mac OS X. - self.drawInRect(rect, blendMode: .DestinationIn, alpha: 1.0) + self.draw(in: rect, blendMode: .destinationIn, alpha: 1.0) let image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); if (self.capInsets.bottom != 0 || self.capInsets.top != 0 || self.capInsets.left != 0 || self.capInsets.right != 0) { - return image.resizableImageWithCapInsets(capInsets, resizingMode: resizingMode) + return image!.resizableImage(withCapInsets: capInsets, resizingMode: resizingMode) } - return image.imageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal) + return image!.withRenderingMode(UIImageRenderingMode.alwaysOriginal) } - public func tintBgImage(color: UIColor) -> UIImage { - UIGraphicsBeginImageContextWithOptions(self.size,false,UIScreen.mainScreen().scale); + public func tintBgImage(_ color: UIColor) -> UIImage { + UIGraphicsBeginImageContextWithOptions(self.size,false,UIScreen.main.scale); - var rect = CGRectZero; + var rect = CGRect.zero; rect.size = self.size; // Composite tint color at its own opacity. color.set(); @@ -51,133 +51,133 @@ public extension UIImage { // Mask tint color-swatch to this image's opaque mask. // We want behaviour like NSCompositeDestinationIn on Mac OS X. - self.drawInRect(rect, blendMode: .Overlay, alpha: 1.0) + self.draw(in: rect, blendMode: .overlay, alpha: 1.0) let image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - return image + return image! } - public func roundImage(newSize: Int) -> UIImage { + public func roundImage(_ newSize: Int) -> UIImage { let nSize = CGSize(width: newSize, height: newSize) - UIGraphicsBeginImageContextWithOptions(nSize,false,UIScreen.mainScreen().scale); + UIGraphicsBeginImageContextWithOptions(nSize,false,UIScreen.main.scale); let context = UIGraphicsGetCurrentContext(); // Background - CGContextAddPath(context, CGPathCreateWithEllipseInRect(CGRect(origin: CGPointZero, size: nSize),nil)); - CGContextClip(context); - self.drawInRect(CGRect(origin: CGPointMake(-1, -1), size: CGSize(width: (newSize+2), height: (newSize+2)))); + context?.addPath(CGPath(ellipseIn: CGRect(origin: CGPoint.zero, size: nSize),transform: nil)); + context?.clip(); + self.draw(in: CGRect(origin: CGPoint(x: -1, y: -1), size: CGSize(width: (newSize+2), height: (newSize+2)))); // Border - CGContextSetStrokeColorWithColor(context, UIColor(red: 0, green: 0, blue: 0, alpha: 0x19/255.0).CGColor); - CGContextAddArc(context,CGFloat(newSize)/2, CGFloat(newSize)/2, CGFloat(newSize)/2, CGFloat(M_PI * 0), CGFloat(M_PI * 2), 0); - CGContextDrawPath(context, .Stroke); + context?.setStrokeColor(UIColor(red: 0, green: 0, blue: 0, alpha: 0x19/255.0).cgColor); + // context?.addArc(CGFloat(newSize)/2, CGFloat(newSize)/2, CGFloat(newSize)/2, CGFloat(M_PI * 0), CGFloat(M_PI * 2), 0); + context?.drawPath(using: .stroke); let image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - return image; + return image!; } - public func roundCorners(w: CGFloat, h: CGFloat, roundSize: CGFloat) -> UIImage { + public func roundCorners(_ w: CGFloat, h: CGFloat, roundSize: CGFloat) -> UIImage { let nSize = CGSize(width: w, height: h) - UIGraphicsBeginImageContextWithOptions(nSize, false, UIScreen.mainScreen().scale); + UIGraphicsBeginImageContextWithOptions(nSize, false, UIScreen.main.scale); // Background - UIBezierPath(roundedRect: CGRectMake(0, 0, w, h), cornerRadius: roundSize).addClip() + UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: w, height: h), cornerRadius: roundSize).addClip() - self.drawInRect(CGRect(origin: CGPointMake(-1, -1), size: CGSize(width: (w+2), height: (h+2)))); + self.draw(in: CGRect(origin: CGPoint(x: -1, y: -1), size: CGSize(width: (w+2), height: (h+2)))); let image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - return image; + return image!; } - public func resizeSquare(maxW: CGFloat, maxH: CGFloat) -> UIImage { + public func resizeSquare(_ maxW: CGFloat, maxH: CGFloat) -> UIImage { let realW = self.size.width / self.scale; let realH = self.size.height / self.scale; let factor = min(maxW/realW, maxH/realH) return resize(factor * realW, h: factor * realH) } - func resizeOptimize(maxPixels: Int) -> UIImage { + func resizeOptimize(_ maxPixels: Int) -> UIImage { let realW = self.size.width / self.scale; let realH = self.size.height / self.scale; let factor = min(1.0, CGFloat(maxPixels) / (realW * realH)); return resize(factor * realW, h: factor * realH) } - public func resize(w: CGFloat, h: CGFloat) -> UIImage { + public func resize(_ w: CGFloat, h: CGFloat) -> UIImage { let nSize = CGSize(width: w, height: h) UIGraphicsBeginImageContextWithOptions(nSize, false, 1.0) - drawInRect(CGRect(origin: CGPointMake(-1, -1), size: CGSize(width: (w + 2), height: (h + 2)))) + draw(in: CGRect(origin: CGPoint(x: -1, y: -1), size: CGSize(width: (w + 2), height: (h + 2)))) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() - return image; + return image!; } - public func aa_imageWithColor(color1: UIColor) -> UIImage { + public func aa_imageWithColor(_ color1: UIColor) -> UIImage { UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale) let context = UIGraphicsGetCurrentContext() - CGContextTranslateCTM(context, 0, self.size.height) - CGContextScaleCTM(context, 1.0, -1.0); - CGContextSetBlendMode(context, CGBlendMode.Normal) + context?.translateBy(x: 0, y: self.size.height) + context?.scaleBy(x: 1.0, y: -1.0); + context?.setBlendMode(CGBlendMode.normal) - let rect = CGRectMake(0, 0, self.size.width, self.size.height) as CGRect - CGContextClipToMask(context, rect, self.CGImage) + let rect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height) as CGRect + context?.clip(to: rect, mask: self.cgImage!) color1.setFill() - CGContextFillRect(context, rect) + context?.fill(rect) - let newImage = UIGraphicsGetImageFromCurrentImageContext() as UIImage + let newImage = UIGraphicsGetImageFromCurrentImageContext()! as UIImage UIGraphicsEndImageContext() return newImage } } -public class Imaging { +open class Imaging { - public class func roundedImage(color: UIColor, radius: CGFloat) -> UIImage { - return roundedImage(color, size: CGSizeMake(radius * 2, radius * 2), radius: radius) + open class func roundedImage(_ color: UIColor, radius: CGFloat) -> UIImage { + return roundedImage(color, size: CGSize(width: radius * 2, height: radius * 2), radius: radius) } - public class func roundedImage(color: UIColor, size: CGSize, radius: CGFloat) -> UIImage { + open class func roundedImage(_ color: UIColor, size: CGSize, radius: CGFloat) -> UIImage { UIGraphicsBeginImageContextWithOptions(size, false, 0) - let path = UIBezierPath(roundedRect: CGRectMake(0, 0, size.width, size.height), cornerRadius: radius) + let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: size.width, height: size.height), cornerRadius: radius) path.lineWidth = 1 color.setFill() path.fill() - let image: UIImage = UIGraphicsGetImageFromCurrentImageContext() + let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() - return image.resizableImageWithCapInsets(UIEdgeInsetsMake(radius, radius, radius, radius)) + return image.resizableImage(withCapInsets: UIEdgeInsetsMake(radius, radius, radius, radius)) } - public class func circleImage(color: UIColor, radius: CGFloat) -> UIImage { - return circleImage(color, size: CGSizeMake(radius * 2, radius * 2), radius: radius) + open class func circleImage(_ color: UIColor, radius: CGFloat) -> UIImage { + return circleImage(color, size: CGSize(width: radius * 2, height: radius * 2), radius: radius) } - public class func circleImage(color: UIColor, size: CGSize, radius: CGFloat) -> UIImage { + open class func circleImage(_ color: UIColor, size: CGSize, radius: CGFloat) -> UIImage { UIGraphicsBeginImageContextWithOptions(size, false, 0) - let path = UIBezierPath(roundedRect: CGRectMake(1, 1, size.width - 2, size.height - 2), cornerRadius: radius) + let path = UIBezierPath(roundedRect: CGRect(x: 1, y: 1, width: size.width - 2, height: size.height - 2), cornerRadius: radius) color.setStroke() path.stroke() - let image: UIImage = UIGraphicsGetImageFromCurrentImageContext() + let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() - return image.resizableImageWithCapInsets(UIEdgeInsetsMake(radius, radius, radius, radius)) + return image.resizableImage(withCapInsets: UIEdgeInsetsMake(radius, radius, radius, radius)) } - public class func imageWithColor(color: UIColor, size: CGSize) -> UIImage { - let rect = CGRectMake(0, 0, size.width, size.height) + open class func imageWithColor(_ color: UIColor, size: CGSize) -> UIImage { + let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) UIGraphicsBeginImageContextWithOptions(size, false, 0) color.setFill() UIRectFill(rect) - let image: UIImage = UIGraphicsGetImageFromCurrentImageContext() + let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return image } @@ -198,11 +198,11 @@ public extension UIImage { return applyBlurWithRadius(20, tintColor: UIColor(white: 0.11, alpha: 0.73), saturationDeltaFactor: 1.8) } - public func applyTintEffectWithColor(tintColor: UIColor) -> UIImage? { + public func applyTintEffectWithColor(_ tintColor: UIColor) -> UIImage? { let effectColorAlpha: CGFloat = 0.6 var effectColor = tintColor - let componentCount = CGColorGetNumberOfComponents(tintColor.CGColor) + let componentCount = tintColor.cgColor.numberOfComponents if componentCount == 2 { var b: CGFloat = 0 @@ -222,39 +222,39 @@ public extension UIImage { return applyBlurWithRadius(10, tintColor: effectColor, saturationDeltaFactor: -1.0, maskImage: nil) } - public func applyBlur(blurRadius: CGFloat) -> UIImage? { + public func applyBlur(_ blurRadius: CGFloat) -> UIImage? { return applyBlurWithRadius(blurRadius, tintColor: nil, saturationDeltaFactor: 1.0) } - public func applyBlurWithRadius(blurRadius: CGFloat, tintColor: UIColor?, saturationDeltaFactor: CGFloat, maskImage: UIImage? = nil) -> UIImage? { + public func applyBlurWithRadius(_ blurRadius: CGFloat, tintColor: UIColor?, saturationDeltaFactor: CGFloat, maskImage: UIImage? = nil) -> UIImage? { // Check pre-conditions. if (size.width < 1 || size.height < 1) { print("*** error: invalid size: \(size.width) x \(size.height). Both dimensions must be >= 1: \(self)") return nil } - if self.CGImage == nil { + if self.cgImage == nil { print("*** error: image must be backed by a CGImage: \(self)") return nil } - if maskImage != nil && maskImage!.CGImage == nil { + if maskImage != nil && maskImage!.cgImage == nil { print("*** error: maskImage must be backed by a CGImage: \(maskImage)") return nil } let __FLT_EPSILON__ = CGFloat(FLT_EPSILON) - let screenScale = UIScreen.mainScreen().scale - let imageRect = CGRect(origin: CGPointZero, size: size) + let screenScale = UIScreen.main.scale + let imageRect = CGRect(origin: CGPoint.zero, size: size) var effectImage = self let hasBlur = blurRadius > __FLT_EPSILON__ let hasSaturationChange = fabs(saturationDeltaFactor - 1.0) > __FLT_EPSILON__ if hasBlur || hasSaturationChange { - func createEffectBuffer(context: CGContext) -> vImage_Buffer { - let data = CGBitmapContextGetData(context) - let width = vImagePixelCount(CGBitmapContextGetWidth(context)) - let height = vImagePixelCount(CGBitmapContextGetHeight(context)) - let rowBytes = CGBitmapContextGetBytesPerRow(context) + func createEffectBuffer(_ context: CGContext) -> vImage_Buffer { + let data = context.data + let width = vImagePixelCount(context.width) + let height = vImagePixelCount(context.height) + let rowBytes = context.bytesPerRow return vImage_Buffer(data: data, height: height, width: width, rowBytes: rowBytes) } @@ -262,9 +262,9 @@ public extension UIImage { UIGraphicsBeginImageContextWithOptions(size, false, screenScale) let effectInContext = UIGraphicsGetCurrentContext() - CGContextScaleCTM(effectInContext, 1.0, -1.0) - CGContextTranslateCTM(effectInContext, 0, -size.height) - CGContextDrawImage(effectInContext, imageRect, self.CGImage) + effectInContext?.scaleBy(x: 1.0, y: -1.0) + effectInContext?.translateBy(x: 0, y: -size.height) + effectInContext?.draw(self.cgImage!, in: imageRect) var effectInBuffer = createEffectBuffer(effectInContext!) @@ -290,7 +290,9 @@ public extension UIImage { // let inputRadius = blurRadius * screenScale - var radius = UInt32(floor(inputRadius * 3.0 * CGFloat(sqrt(2 * M_PI)) / 4 + 0.5)) + let radiusPi = CGFloat(sqrt(2 * M_PI)) + let radiusFl = floor(inputRadius * 3.0 * radiusPi / 4 + 0.5) + var radius = UInt32(radiusFl) if radius % 2 != 1 { radius += 1 // force radius to be odd so that the three box-blur methodology works. } @@ -315,7 +317,7 @@ public extension UIImage { let divisor: CGFloat = 256 let matrixSize = floatingPointSaturationMatrix.count - var saturationMatrix = [Int16](count: matrixSize, repeatedValue: 0) + var saturationMatrix = [Int16](repeating: 0, count: matrixSize) for i: Int in 0 ..< matrixSize { saturationMatrix[i] = Int16(round(floatingPointSaturationMatrix[i] * divisor)) @@ -330,13 +332,13 @@ public extension UIImage { } if !effectImageBuffersAreSwapped { - effectImage = UIGraphicsGetImageFromCurrentImageContext() + effectImage = UIGraphicsGetImageFromCurrentImageContext()! } UIGraphicsEndImageContext() if effectImageBuffersAreSwapped { - effectImage = UIGraphicsGetImageFromCurrentImageContext() + effectImage = UIGraphicsGetImageFromCurrentImageContext()! } UIGraphicsEndImageContext() @@ -345,28 +347,28 @@ public extension UIImage { // Set up output context. UIGraphicsBeginImageContextWithOptions(size, false, screenScale) let outputContext = UIGraphicsGetCurrentContext() - CGContextScaleCTM(outputContext, 1.0, -1.0) - CGContextTranslateCTM(outputContext, 0, -size.height) + outputContext?.scaleBy(x: 1.0, y: -1.0) + outputContext?.translateBy(x: 0, y: -size.height) // Draw base image. - CGContextDrawImage(outputContext, imageRect, self.CGImage) + outputContext?.draw(self.cgImage!, in: imageRect) // Draw effect image. if hasBlur { - CGContextSaveGState(outputContext) + outputContext?.saveGState() if let image = maskImage { - CGContextClipToMask(outputContext, imageRect, image.CGImage); + outputContext?.clip(to: imageRect, mask: image.cgImage!); } - CGContextDrawImage(outputContext, imageRect, effectImage.CGImage) - CGContextRestoreGState(outputContext) + outputContext?.draw(effectImage.cgImage!, in: imageRect) + outputContext?.restoreGState() } // Add in color tint. if let color = tintColor { - CGContextSaveGState(outputContext) - CGContextSetFillColorWithColor(outputContext, color.CGColor) - CGContextFillRect(outputContext, imageRect) - CGContextRestoreGState(outputContext) + outputContext?.saveGState() + outputContext?.setFillColor(color.cgColor) + outputContext?.fill(imageRect) + outputContext?.restoreGState() } // Output image is ready. @@ -378,7 +380,7 @@ public extension UIImage { } extension UIImage { - public func imageRotatedByDegrees(degrees: CGFloat, flip: Bool) -> UIImage { + public func imageRotatedByDegrees(_ degrees: CGFloat, flip: Bool) -> UIImage { // let radiansToDegrees: (CGFloat) -> CGFloat = { // return $0 * (180.0 / CGFloat(M_PI)) // } @@ -387,8 +389,8 @@ extension UIImage { } // calculate the size of the rotated view's containing box for our drawing space - let rotatedViewBox = UIView(frame: CGRect(origin: CGPointZero, size: size)) - let t = CGAffineTransformMakeRotation(degreesToRadians(degrees)); + let rotatedViewBox = UIView(frame: CGRect(origin: CGPoint.zero, size: size)) + let t = CGAffineTransform(rotationAngle: degreesToRadians(degrees)); rotatedViewBox.transform = t let rotatedSize = rotatedViewBox.frame.size @@ -397,10 +399,10 @@ extension UIImage { let bitmap = UIGraphicsGetCurrentContext() // Move the origin to the middle of the image so we will rotate and scale around the center. - CGContextTranslateCTM(bitmap, rotatedSize.width / 2.0, rotatedSize.height / 2.0); + bitmap?.translateBy(x: rotatedSize.width / 2.0, y: rotatedSize.height / 2.0); // // Rotate the image context - CGContextRotateCTM(bitmap, degreesToRadians(degrees)); + bitmap?.rotate(by: degreesToRadians(degrees)); // Now, draw the rotated/scaled image into the context var yFlip: CGFloat @@ -411,13 +413,13 @@ extension UIImage { yFlip = CGFloat(1.0) } - CGContextScaleCTM(bitmap, yFlip, -1.0) - CGContextDrawImage(bitmap, CGRectMake(-size.width / 2, -size.height / 2, size.width, size.height), CGImage) + bitmap?.scaleBy(x: yFlip, y: -1.0) + bitmap?.draw(cgImage!, in: CGRect(x: -size.width / 2, y: -size.height / 2, width: size.width, height: size.height)) let newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() - return newImage + return newImage! } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Logs.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Logs.swift index 4417bb87f4..480ce1e341 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Logs.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Logs.swift @@ -4,6 +4,6 @@ import UIKit -func log(text:String) { +func log(_ text:String) { NSLog(text) -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Numbers.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Numbers.swift index b1b91522fa..9f3523f269 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Numbers.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Numbers.swift @@ -5,19 +5,19 @@ import Foundation public extension Int { - public func format(f: String) -> String { - return NSString(format: "%\(f)d", self) as String + public func format(_ f: String) -> String { + return NSString(format: "%\(f)d" as NSString, self) as String } } public extension Double { - public func format(f: String) -> String { - return NSString(format: "%\(f)f", self) as String + public func format(_ f: String) -> String { + return NSString(format: "%\(f)f" as NSString, self) as String } } -public extension NSTimeInterval { +public extension TimeInterval { public var time:String { - return String(format:"%02d:%02d:%02d.%03d", Int((self/3600.0)%60),Int((self/60.0)%60), Int((self) % 60 ), Int(self*1000 % 1000 ) ) + return String(format:"%02d:%02d:%02d.%03d", Int((self/3600.0).truncatingRemainder(dividingBy: 60)),Int((self/60.0).truncatingRemainder(dividingBy: 60)), Int((self).truncatingRemainder(dividingBy: 60) ), Int((self*1000).truncatingRemainder(dividingBy: 1000) ) ) } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Promises.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Promises.swift index c55c2a44c5..f24a2d91b3 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Promises.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Promises.swift @@ -7,14 +7,14 @@ import MBProgressHUD extension ARPromise { - func startUserAction(ignore: [String] = []) -> ARPromise { + func startUserAction(_ ignore: [String] = []) -> ARPromise { - let window = UIApplication.sharedApplication().windows[1] + let window = UIApplication.shared.windows[1] let hud = MBProgressHUD(window: window) - hud.mode = MBProgressHUDMode.Indeterminate + hud.mode = MBProgressHUDMode.indeterminate hud.removeFromSuperViewOnHide = true window.addSubview(hud) - window.bringSubviewToFront(hud) + window.bringSubview(toFront: hud) hud.show(true) then { (t: AnyObject!) -> () in @@ -34,12 +34,17 @@ extension ARPromise { return self } - func then(closure: (T!) -> ()) -> ARPromise { + func then(_ closure: @escaping (T!) -> ()) -> ARPromise { then(PromiseConsumer(closure: closure)) return self } - func failure(withClosure closure: (JavaLangException!) -> ()) -> ARPromise { + func after(_ closure: @escaping () -> ()) -> ARPromise { + then(PromiseConsumerEmpty(closure: closure)) + return self + } + + func failure(withClosure closure: @escaping (JavaLangException!) -> ()) -> ARPromise { failure(PromiseConsumer(closure: closure)) return self } @@ -49,11 +54,24 @@ class PromiseConsumer: NSObject, ARConsumer { let closure: (T!) -> () - init(closure: (T!) -> ()) { + init(closure: @escaping (T!) -> ()) { self.closure = closure } - func applyWithId(t: AnyObject!) { + func apply(withId t: Any!) { closure(t as? T) } -} \ No newline at end of file +} + +class PromiseConsumerEmpty: NSObject, ARConsumer { + + let closure: () -> () + + init(closure: @escaping () -> ()) { + self.closure = closure + } + + func apply(withId t: Any!) { + closure() + } +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Strings.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Strings.swift index 0e718ee9c1..bc70feb3f2 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Strings.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Strings.swift @@ -11,47 +11,47 @@ public extension String { public var length: Int { return self.characters.count } - public func indexOf(str: String) -> Int? { - if let range = rangeOfString(str) { - return startIndex.distanceTo(range.startIndex) + public func indexOf(_ str: String) -> Int? { + if let range = range(of: str) { + return characters.distance(from: startIndex, to: range.lowerBound) } else { return nil } } public func trim() -> String { - return stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet()); + return trimmingCharacters(in: CharacterSet.whitespaces); } public subscript (i: Int) -> Character { - return self[self.startIndex.advancedBy(i)] + return self[self.characters.index(self.startIndex, offsetBy: i)] } public subscript (i: Int) -> String { return String(self[i] as Character) } - public func first(count: Int) -> String { + public func first(_ count: Int) -> String { let realCount = min(count, length); - return substringToIndex(startIndex.advancedBy(realCount)); + return substring(to: characters.index(startIndex, offsetBy: realCount)); } - public func skip(count: Int) -> String { + public func skip(_ count: Int) -> String { let realCount = min(count, length); - return substringFromIndex(startIndex.advancedBy(realCount)) + return substring(from: characters.index(startIndex, offsetBy: realCount)) } - public func strip(set: NSCharacterSet) -> String { - return componentsSeparatedByCharactersInSet(set).joinWithSeparator("") + public func strip(_ set: CharacterSet) -> String { + return components(separatedBy: set).joined(separator: "") } - public func replace(src: String, dest:String) -> String { - return stringByReplacingOccurrencesOfString(src, withString: dest, options: NSStringCompareOptions(), range: nil) + public func replace(_ src: String, dest:String) -> String { + return replacingOccurrences(of: src, with: dest, options: NSString.CompareOptions(), range: nil) } public func toLong() -> Int64? { - return NSNumberFormatter().numberFromString(self)?.longLongValue + return NumberFormatter().number(from: self)?.int64Value } public func toJLong() -> jlong { @@ -63,46 +63,46 @@ public extension String { if (trimmed.isEmpty){ return "#"; } - let letters = NSCharacterSet.letterCharacterSet() + let letters = CharacterSet.letters let res: String = self[0]; - if (res.rangeOfCharacterFromSet(letters) != nil) { - return res.uppercaseString; + if (res.rangeOfCharacter(from: letters) != nil) { + return res.uppercased(); } else { return "#"; } } - public func hasPrefixInWords(prefix: String) -> Bool { - var components = self.componentsSeparatedByString(" ") + public func hasPrefixInWords(_ prefix: String) -> Bool { + var components = self.components(separatedBy: " ") for i in 0.. Bool { - return self.rangeOfString(text, options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil, locale: nil) != nil + public func contains(_ text: String) -> Bool { + return self.range(of: text, options: NSString.CompareOptions.caseInsensitive, range: nil, locale: nil) != nil } - public func startsWith(text: String) -> Bool { - let range = rangeOfString(text) + public func startsWith(_ text: String) -> Bool { + let range = self.range(of: text) if range != nil { - return range!.startIndex == startIndex + return range!.lowerBound == startIndex } return false } - public func rangesOfString(text: String) -> [Range] { + public func rangesOfString(_ text: String) -> [Range] { var res = [Range]() - var searchRange = Range(start: self.startIndex, end: self.endIndex) + var searchRange = (self.startIndex ..< self.endIndex) while true { - let found = self.rangeOfString(text, options: NSStringCompareOptions.CaseInsensitiveSearch, range: searchRange, locale: nil) + let found = self.range(of: text, options: String.CompareOptions.caseInsensitive, range: searchRange, locale: nil) if found != nil { res.append(found!) - searchRange = Range(start: found!.endIndex, end: self.endIndex) + searchRange = (found!.upperBound ..< self.endIndex) } else { break } @@ -111,7 +111,7 @@ public extension String { return res } - public func repeatString(count: Int) -> String { + public func repeatString(_ count: Int) -> String { var res = "" for _ in 0.. Bool { - if let url = NSURL(string: self) { - return UIApplication.sharedApplication().canOpenURL(url) + if let url = URL(string: self) { + return UIApplication.shared.canOpenURL(url) } return false } @@ -141,15 +141,15 @@ public extension String { public extension NSAttributedString { - public func append(text: NSAttributedString) -> NSAttributedString { + public func appendMutate(_ text: NSAttributedString) -> NSAttributedString { let res = NSMutableAttributedString() - res.appendAttributedString(self) - res.appendAttributedString(text) + res.append(self) + res.append(text) return res } - public func append(text: String, font: UIFont) -> NSAttributedString { - return append(NSAttributedString(string: text, attributes: [NSFontAttributeName: font])) + public func appendMutate(_ text: String, font: UIFont) -> NSAttributedString { + return self.appendMutate(NSAttributedString(string: text, attributes: [NSFontAttributeName: font])) } public convenience init(string: String, font: UIFont) { @@ -159,12 +159,12 @@ public extension NSAttributedString { public extension NSMutableAttributedString { - public func appendFont(font: UIFont) { + public func appendFont(_ font: UIFont) { self.addAttribute(NSFontAttributeName, value: font, range: NSMakeRange(0, self.length)) } - public func appendColor(color: UIColor) { - self.addAttribute(NSForegroundColorAttributeName, value: color.CGColor, range: NSMakeRange(0, self.length)) + public func appendColor(_ color: UIColor) { + self.addAttribute(NSForegroundColorAttributeName, value: color.cgColor, range: NSMakeRange(0, self.length)) } } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Views.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Views.swift index c4ec3502e4..04779a14ee 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Views.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/SwiftExtensions/Views.swift @@ -9,7 +9,7 @@ import AVFoundation private var targetReference = "target" public extension UITapGestureRecognizer { - public convenience init(closure: ()->()){ + public convenience init(closure: @escaping ()->()){ let target = ClosureTarget(closure: closure) self.init(target: target, action: #selector(ClosureTarget.invoke)) setAssociatedObject(self, value: target, associativeKey: &targetReference) @@ -21,7 +21,7 @@ public extension UIView { set (value) { if value != nil { self.addGestureRecognizer(UITapGestureRecognizer(closure: value!)) - self.userInteractionEnabled = true + self.isUserInteractionEnabled = true } } get { @@ -32,9 +32,9 @@ public extension UIView { private class ClosureTarget { - private let closure: ()->() + fileprivate let closure: ()->() - init(closure: ()->()) { + init(closure: @escaping ()->()) { self.closure = closure } @@ -48,43 +48,43 @@ private class ClosureTarget { public extension UIView { public func hideView() { - self.hidden = true + self.isHidden = true // UIView.animateWithDuration(0.2, animations: { () -> Void in // self.alpha = 0 // }) } public func showView() { - self.hidden = false + self.isHidden = false // UIView.animateWithDuration(0.2, animations: { () -> Void in // self.alpha = 1 // }) } public func showViewAnimated() { - UIView.animateWithDuration(0.2, animations: { () -> Void in + UIView.animate(withDuration: 0.2, animations: { () -> Void in self.alpha = 1 }) } public func hideViewAnimated() { - UIView.animateWithDuration(0.2, animations: { () -> Void in + UIView.animate(withDuration: 0.2, animations: { () -> Void in self.alpha = 0 }) } public func showViewPop() { - UIView.animateWithDuration(1, delay: 0, usingSpringWithDamping: 0.7, - initialSpringVelocity: 0.6, options: .CurveEaseOut, animations: { () -> Void in - self.transform = CGAffineTransformIdentity; + UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 0.7, + initialSpringVelocity: 0.6, options: .curveEaseOut, animations: { () -> Void in + self.transform = CGAffineTransform.identity; self.alpha = 1 }, completion: nil) } public func hideViewPop() { - UIView.animateWithDuration(1, delay: 0, usingSpringWithDamping: 0.7, - initialSpringVelocity: 1.0,options: .CurveEaseOut,animations: { () -> Void in - self.transform = CGAffineTransformMakeScale(0.01, 0.01) + UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 0.7, + initialSpringVelocity: 1.0,options: .curveEaseOut,animations: { () -> Void in + self.transform = CGAffineTransform(scaleX: 0.01, y: 0.01) self.alpha = 0 }, completion: nil) } @@ -97,53 +97,53 @@ public extension UIView { // public var top: CGFloat { get { return frame.minY } } // public var bottom: CGFloat { get { return frame.maxY } } - public func centerIn(rect: CGRect) { - self.frame = CGRectMake(rect.origin.x + (rect.width - self.bounds.width) / 2, rect.origin.y + (rect.height - self.bounds.height) / 2, - self.bounds.width, self.bounds.height) + public func centerIn(_ rect: CGRect) { + self.frame = CGRect(x: rect.origin.x + (rect.width - self.bounds.width) / 2, y: rect.origin.y + (rect.height - self.bounds.height) / 2, + width: self.bounds.width, height: self.bounds.height) } - public func under(rect: CGRect, offset: CGFloat) { - self.frame = CGRectMake(rect.origin.x + (rect.width - self.bounds.width) / 2, rect.origin.y + rect.height + offset, - self.bounds.width, self.bounds.height) + public func under(_ rect: CGRect, offset: CGFloat) { + self.frame = CGRect(x: rect.origin.x + (rect.width - self.bounds.width) / 2, y: rect.origin.y + rect.height + offset, + width: self.bounds.width, height: self.bounds.height) } - public func topIn(rect: CGRect) { - self.frame = CGRectMake(rect.origin.x + (rect.width - self.bounds.width) / 2, rect.origin.y, - self.bounds.width, self.bounds.height) + public func topIn(_ rect: CGRect) { + self.frame = CGRect(x: rect.origin.x + (rect.width - self.bounds.width) / 2, y: rect.origin.y, + width: self.bounds.width, height: self.bounds.height) } } // Text measuring -public class UIViewMeasure { +open class UIViewMeasure { - public class func measureText(text: String, width: CGFloat, fontSize: CGFloat) -> CGSize { - return UIViewMeasure.measureText(text, width: width, font: UIFont.systemFontOfSize(fontSize)) + open class func measureText(_ text: String, width: CGFloat, fontSize: CGFloat) -> CGSize { + return UIViewMeasure.measureText(text, width: width, font: UIFont.systemFont(ofSize: fontSize)) } - public class func measureText(text: String, width: CGFloat, font: UIFont) -> CGSize { + open class func measureText(_ text: String, width: CGFloat, font: UIFont) -> CGSize { // Building paragraph styles let style = NSMutableParagraphStyle() - style.lineBreakMode = NSLineBreakMode.ByWordWrapping + style.lineBreakMode = NSLineBreakMode.byWordWrapping // Measuring text with reduced width - let rect = text.boundingRectWithSize(CGSize(width: width - 2, height: CGFloat.max), - options: NSStringDrawingOptions.UsesLineFragmentOrigin, + let rect = text.boundingRect(with: CGSize(width: width - 2, height: CGFloat.greatestFiniteMagnitude), + options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSFontAttributeName: font, NSParagraphStyleAttributeName: style], context: nil) // Returning size with expanded width - return CGSizeMake(ceil(rect.width + 2), CGFloat(ceil(rect.height))) + return CGSize(width: ceil(rect.width + 2), height: CGFloat(ceil(rect.height))) } - public class func measureText(attributedText: NSAttributedString, width: CGFloat) -> CGSize { + open class func measureText(_ attributedText: NSAttributedString, width: CGFloat) -> CGSize { // Measuring text with reduced width - let rect = attributedText.boundingRectWithSize(CGSize(width: width - 2, height: CGFloat.max), options: [.UsesLineFragmentOrigin, .UsesFontLeading], context: nil) + let rect = attributedText.boundingRect(with: CGSize(width: width - 2, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil) // Returning size with expanded width and height - return CGSizeMake(ceil(rect.width + 2), CGFloat(ceil(rect.height))) + return CGSize(width: ceil(rect.width + 2), height: CGFloat(ceil(rect.height))) } } @@ -152,7 +152,7 @@ public class UIViewMeasure { private var registeredCells = "cells!" public extension UITableView { - private func cellTypeForClass(cellClass: AnyClass) -> String { + fileprivate func cellTypeForClass(_ cellClass: AnyClass) -> String { let cellReuseId = "\(cellClass)" var registered: ([String])! = getAssociatedObject(self, associativeKey: ®isteredCells) var found = false @@ -168,30 +168,30 @@ public extension UITableView { } if !found { - registerClass(cellClass, forCellReuseIdentifier: cellReuseId) + register(cellClass, forCellReuseIdentifier: cellReuseId) } return cellReuseId } - public func dequeueCell(cellClass: AnyClass, indexPath: NSIndexPath) -> UITableViewCell { + public func dequeueCell(_ cellClass: AnyClass, indexPath: IndexPath) -> UITableViewCell { let reuseId = cellTypeForClass(cellClass) - return self.dequeueReusableCellWithIdentifier(reuseId, forIndexPath: indexPath) + return self.dequeueReusableCell(withIdentifier: reuseId, for: indexPath) } - public func dequeueCell(indexPath: NSIndexPath) -> T { + public func dequeueCell(_ indexPath: IndexPath) -> T where T: UITableViewCell { let reuseId = cellTypeForClass(T.self) - return self.dequeueReusableCellWithIdentifier(reuseId, forIndexPath: indexPath) as! T + return self.dequeueReusableCell(withIdentifier: reuseId, for: indexPath) as! T } - public func visibleCellForIndexPath(path: NSIndexPath) -> UITableViewCell? { + public func visibleCellForIndexPath(_ path: IndexPath) -> UITableViewCell? { if indexPathsForVisibleRows == nil { return nil } for i in 0.. String { + func encodeText(_ key: Int32) -> String { var res = "" for i in 0.. UIImageView? { - if view.isKindOfClass(UIImageView) && view.bounds.height <= 1.0 { + fileprivate func hairlineImageViewInNavigationBar(_ view: UIView) -> UIImageView? { + if view.isKind(of: UIImageView.self) && view.bounds.height <= 1.0 { return (view as! UIImageView) } @@ -369,52 +369,52 @@ extension UINavigationBar { extension AVAsset { func videoOrientation() -> (orientation: UIInterfaceOrientation, device: AVCaptureDevicePosition) { - var orientation: UIInterfaceOrientation = .Unknown - var device: AVCaptureDevicePosition = .Unspecified + var orientation: UIInterfaceOrientation = .unknown + var device: AVCaptureDevicePosition = .unspecified - let tracks :[AVAssetTrack] = self.tracksWithMediaType(AVMediaTypeVideo) + let tracks :[AVAssetTrack] = self.tracks(withMediaType: AVMediaTypeVideo) if let videoTrack = tracks.first { let t = videoTrack.preferredTransform if (t.a == 0 && t.b == 1.0 && t.d == 0) { - orientation = .Portrait + orientation = .portrait if t.c == 1.0 { - device = .Front + device = .front } else if t.c == -1.0 { - device = .Back + device = .back } } else if (t.a == 0 && t.b == -1.0 && t.d == 0) { - orientation = .PortraitUpsideDown + orientation = .portraitUpsideDown if t.c == -1.0 { - device = .Front + device = .front } else if t.c == 1.0 { - device = .Back + device = .back } } else if (t.a == 1.0 && t.b == 0 && t.c == 0) { - orientation = .LandscapeRight + orientation = .landscapeRight if t.d == -1.0 { - device = .Front + device = .front } else if t.d == 1.0 { - device = .Back + device = .back } } else if (t.a == -1.0 && t.b == 0 && t.c == 0) { - orientation = .LandscapeLeft + orientation = .landscapeLeft if t.d == 1.0 { - device = .Front + device = .front } else if t.d == -1.0 { - device = .Back + device = .back } } } return (orientation, device) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AAAudioManager.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AAAudioManager.swift index d8fdcdc42a..6e813cb635 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AAAudioManager.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AAAudioManager.swift @@ -5,39 +5,41 @@ import Foundation import AVFoundation -public class AAAudioManager: NSObject, AVAudioPlayerDelegate { +open class AAAudioManager: NSObject, AVAudioPlayerDelegate { - private static let sharedManager = AAAudioManager() + fileprivate static let sharedManager = AAAudioManager() - public static func sharedAudio() -> AAAudioManager { + open static func sharedAudio() -> AAAudioManager { return sharedManager } - private var isRinging = false - private var ringtonePlaying = false - private var ringtonePlayer: AVAudioPlayer! = nil - private var audioRouter = AAAudioRouter() + fileprivate var isRinging = false + fileprivate var ringtonePlaying = false + fileprivate var ringtonePlayer: AVAudioPlayer! = nil + fileprivate var audioRouter = AAAudioRouter() - private var ringtoneSound:SystemSoundID = 0 - private var isVisible = false + fileprivate var ringtoneSound:SystemSoundID = 0 + fileprivate var isVisible = false - private var isEnabled: Bool = false - private var openedConnections: Int = 0 + fileprivate var isEnabled: Bool = false + fileprivate var isVideoPreferred = false + fileprivate var openedConnections: Int = 0 public override init() { - super.init() - + super.init() } - public func appVisible() { + open func appVisible() { isVisible = true } - public func appHidden() { + open func appHidden() { isVisible = false } - public func callStart(call: ACCallVM) { + open func callStart(_ call: ACCallVM) { + isVideoPreferred = call.isVideoPreferred + if !call.isOutgoing { isRinging = true if isVisible { @@ -45,8 +47,8 @@ public class AAAudioManager: NSObject, AVAudioPlayerDelegate { audioRouter.batchedUpdate { audioRouter.category = AVAudioSessionCategoryPlayAndRecord audioRouter.mode = AVAudioSessionModeDefault - audioRouter.currentRoute = .Speaker - audioRouter.isEnabled = isEnabled + audioRouter.currentRoute = .speaker + audioRouter.isEnabled = true } ringtoneStart() } else { @@ -55,43 +57,57 @@ public class AAAudioManager: NSObject, AVAudioPlayerDelegate { vibrate() } else { isEnabled = true - audioRouter.category = AVAudioSessionCategoryPlayAndRecord - audioRouter.mode = AVAudioSessionModeVoiceChat - audioRouter.currentRoute = .Receiver - audioRouter.isEnabled = isEnabled + audioRouter.batchedUpdate { + audioRouter.category = AVAudioSessionCategoryPlayAndRecord + audioRouter.mode = AVAudioSessionModeVoiceChat + if isVideoPreferred { + audioRouter.currentRoute = .speaker + } else { + audioRouter.currentRoute = .receiver + } + audioRouter.isEnabled = true + } } } - public func callAnswered(call: ACCallVM) { + open func callAnswered(_ call: ACCallVM) { ringtoneEnd() isRinging = false - audioRouter.mode = AVAudioSessionModeVoiceChat - audioRouter.currentRoute = .Receiver + audioRouter.batchedUpdate { + audioRouter.mode = AVAudioSessionModeVoiceChat + if isVideoPreferred { + audioRouter.currentRoute = .speaker + } else { + audioRouter.currentRoute = .receiver + } + } } - public func callEnd(call: ACCallVM) { + open func callEnd(_ call: ACCallVM) { ringtoneEnd() isRinging = false isEnabled = false - audioRouter.category = AVAudioSessionCategorySoloAmbient - audioRouter.mode = AVAudioSessionModeDefault - audioRouter.currentRoute = .Receiver - audioRouter.isEnabled = isEnabled + audioRouter.batchedUpdate { + audioRouter.category = AVAudioSessionCategorySoloAmbient + audioRouter.mode = AVAudioSessionModeDefault + audioRouter.currentRoute = .receiver + audioRouter.isEnabled = false + } } - public func peerConnectionStarted() { + open func peerConnectionStarted() { openedConnections += 1 print("📡 AudioManager: peerConnectionStarted \(self.openedConnections)") audioRouter.isRTCEnabled = openedConnections > 0 } - public func peerConnectionEnded() { + open func peerConnectionEnded() { openedConnections -= 1 print("📡 AudioManager: peerConnectionEnded \(self.openedConnections)") audioRouter.isRTCEnabled = openedConnections > 0 } - private func ringtoneStart() { + fileprivate func ringtoneStart() { if ringtonePlaying { return } @@ -99,7 +115,7 @@ public class AAAudioManager: NSObject, AVAudioPlayerDelegate { ringtonePlaying = true do { - self.ringtonePlayer = try AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: NSBundle.framework.pathForResource("ringtone", ofType: "m4a")!)) + self.ringtonePlayer = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: Bundle.framework.path(forResource: "ringtone", ofType: "m4a")!)) self.ringtonePlayer.delegate = self self.ringtonePlayer.numberOfLoops = -1 self.ringtonePlayer.volume = 1.0 @@ -110,11 +126,11 @@ public class AAAudioManager: NSObject, AVAudioPlayerDelegate { } } - private func vibrate() { + fileprivate func vibrate() { if #available(iOS 9.0, *) { AudioServicesPlayAlertSoundWithCompletion(1352) { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC)), dispatch_get_main_queue()) { () -> Void in + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) { () -> Void in if self.isRinging { self.vibrate() } @@ -122,7 +138,7 @@ public class AAAudioManager: NSObject, AVAudioPlayerDelegate { } } else { AudioServicesPlayAlertSound(1352) - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC)), dispatch_get_main_queue()) { () -> Void in + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) { () -> Void in if self.isRinging { self.vibrate() } @@ -130,35 +146,35 @@ public class AAAudioManager: NSObject, AVAudioPlayerDelegate { } } - private func notificationRingtone(call: ACCallVM) { + fileprivate func notificationRingtone(_ call: ACCallVM) { dispatchOnUi() { let notification = UILocalNotification() if call.peer.isGroup { let groupName = Actor.getGroupWithGid(call.peer.peerId).getNameModel().get() - notification.alertBody = AALocalized("CallGroupText").replace("{name}", dest: groupName) + notification.alertBody = AALocalized("CallGroupText").replace("{name}", dest: groupName!) if #available(iOS 8.2, *) { notification.alertTitle = AALocalized("CallGroupTitle") } } else if call.peer.isPrivate { let userName = Actor.getUserWithUid(call.peer.peerId).getNameModel().get() - notification.alertBody = AALocalized("CallPrivateText").replace("{name}", dest: userName) + notification.alertBody = AALocalized("CallPrivateText").replace("{name}", dest: userName!) if #available(iOS 8.2, *) { notification.alertTitle = AALocalized("CallPrivateTitle") } } notification.soundName = "ringtone.m4a" - UIApplication.sharedApplication().presentLocalNotificationNow(notification) + UIApplication.shared.presentLocalNotificationNow(notification) } - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(10 * NSEC_PER_SEC)), dispatch_get_main_queue()) { () -> Void in + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(10 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) { () -> Void in if self.isRinging { self.notificationRingtone(call) } } } - private func ringtoneEnd() { + fileprivate func ringtoneEnd() { if !ringtonePlaying { return } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AAAudioRouter.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AAAudioRouter.swift index aa8a37bff7..855329dd7f 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AAAudioRouter.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AAAudioRouter.swift @@ -6,16 +6,16 @@ import Foundation import AVFoundation public enum Route { - case Speaker - case Receiver + case speaker + case receiver } -public class AAAudioRouter { +open class AAAudioRouter { - private var isBatchedUpdate = false - private var isInvalidated = false + fileprivate var isBatchedUpdate = false + fileprivate var isInvalidated = false - public var isEnabled = false { + open var isEnabled = false { willSet(v) { if isEnabled != v { isInvalidated = true @@ -26,7 +26,7 @@ public class AAAudioRouter { } } - public var isRTCEnabled = false { + open var isRTCEnabled = false { willSet(v) { if isRTCEnabled != v { isInvalidated = true @@ -37,7 +37,7 @@ public class AAAudioRouter { } } - public var currentRoute = Route.Receiver { + open var currentRoute = Route.receiver { willSet(v) { if currentRoute != v { isInvalidated = true @@ -48,7 +48,7 @@ public class AAAudioRouter { } } - public var mode = AVAudioSessionModeDefault { + open var mode = AVAudioSessionModeDefault { willSet(v) { if mode != v { isInvalidated = true @@ -59,7 +59,7 @@ public class AAAudioRouter { } } - public var category = AVAudioSessionCategorySoloAmbient { + open var category = AVAudioSessionCategorySoloAmbient { willSet(v) { if category != v { isInvalidated = true @@ -72,19 +72,19 @@ public class AAAudioRouter { public init() { fixSession() - NSNotificationCenter.defaultCenter().addObserverForName(AVAudioSessionRouteChangeNotification, - object: nil, queue: NSOperationQueue.mainQueue()) { (note) -> Void in - let notification: NSNotification = note as NSNotification - if let info = notification.userInfo { + NotificationCenter.default.addObserver(forName: NSNotification.Name.AVAudioSessionRouteChange, + object: nil, queue: OperationQueue.main) { (note) -> Void in + let notification: Notification = note as Notification + if let info = (notification as NSNotification).userInfo { let numberReason: NSNumber = info[AVAudioSessionRouteChangeReasonKey] as! NSNumber - if let reason = AVAudioSessionRouteChangeReason(rawValue: UInt(numberReason.integerValue)) { + if let reason = AVAudioSessionRouteChangeReason(rawValue: UInt(numberReason.intValue)) { self.routeChanged(reason) } } } } - func batchedUpdate(@noescape closure: ()->()) { + func batchedUpdate(_ closure: ()->()) { isInvalidated = false isBatchedUpdate = true closure() @@ -95,14 +95,14 @@ public class AAAudioRouter { } } - private func onChanged() { + fileprivate func onChanged() { if !isBatchedUpdate && isInvalidated { isInvalidated = false fixSession() } } - private func fixSession() { + fileprivate func fixSession() { let session = AVAudioSession.sharedInstance() @@ -123,10 +123,10 @@ public class AAAudioRouter { if let route: AVAudioSessionRouteDescription = session.currentRoute { for port in route.outputs { let portDescription: AVAudioSessionPortDescription = port as AVAudioSessionPortDescription - if (self.currentRoute == .Receiver && portDescription.portType != AVAudioSessionPortBuiltInReceiver) { - try session.overrideOutputAudioPort(.None) - } else if (self.currentRoute == .Speaker && portDescription.portType != AVAudioSessionPortBuiltInSpeaker) { - try session.overrideOutputAudioPort(AVAudioSessionPortOverride.Speaker) + if (self.currentRoute == .receiver && portDescription.portType != AVAudioSessionPortBuiltInReceiver) { + try session.overrideOutputAudioPort(.none) + } else if (self.currentRoute == .speaker && portDescription.portType != AVAudioSessionPortBuiltInSpeaker) { + try session.overrideOutputAudioPort(AVAudioSessionPortOverride.speaker) } } } @@ -146,10 +146,10 @@ public class AAAudioRouter { if let route: AVAudioSessionRouteDescription = session.currentRoute { for port in route.outputs { let portDescription: AVAudioSessionPortDescription = port as AVAudioSessionPortDescription - if (self.currentRoute == .Receiver && portDescription.portType != AVAudioSessionPortBuiltInReceiver) { - try session.overrideOutputAudioPort(.None) - } else if (self.currentRoute == .Speaker && portDescription.portType != AVAudioSessionPortBuiltInSpeaker) { - try session.overrideOutputAudioPort(AVAudioSessionPortOverride.Speaker) + if (self.currentRoute == .receiver && portDescription.portType != AVAudioSessionPortBuiltInReceiver) { + try session.overrideOutputAudioPort(.none) + } else if (self.currentRoute == .speaker && portDescription.portType != AVAudioSessionPortBuiltInSpeaker) { + try session.overrideOutputAudioPort(AVAudioSessionPortOverride.speaker) } } } @@ -165,7 +165,7 @@ public class AAAudioRouter { } } - private func isHeadsetPluggedIn() -> Bool { + fileprivate func isHeadsetPluggedIn() -> Bool { let route: AVAudioSessionRouteDescription = AVAudioSession.sharedInstance().currentRoute for port in route.outputs { let portDescription: AVAudioSessionPortDescription = port as AVAudioSessionPortDescription @@ -176,21 +176,21 @@ public class AAAudioRouter { return false } - private func routeChanged(reason: AVAudioSessionRouteChangeReason) { - if reason == .NewDeviceAvailable { + fileprivate func routeChanged(_ reason: AVAudioSessionRouteChangeReason) { + if reason == .newDeviceAvailable { if isHeadsetPluggedIn() { - self.currentRoute = .Receiver + self.currentRoute = .receiver return } - } else if reason == .OldDeviceUnavailable { + } else if reason == .oldDeviceUnavailable { if !isHeadsetPluggedIn() { - self.currentRoute = .Receiver + self.currentRoute = .receiver return } } - if reason == .Override || reason == .RouteConfigurationChange { + if reason == .override || reason == .routeConfigurationChange { fixSession() } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AAFileTypes.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AAFileTypes.swift index 8f8fda3b80..0586af76e7 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AAFileTypes.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AAFileTypes.swift @@ -10,88 +10,88 @@ let UTTAll = [ ] let AAFileTypes = [ - "mp3" :AAFileType.Music, - "m4a" :AAFileType.Music, - "ogg" :AAFileType.Music, - "flac" :AAFileType.Music, - "alac" :AAFileType.Music, - "wav" :AAFileType.Music, - "wma" :AAFileType.Music, - "aac" :AAFileType.Music, + "mp3" :AAFileType.music, + "m4a" :AAFileType.music, + "ogg" :AAFileType.music, + "flac" :AAFileType.music, + "alac" :AAFileType.music, + "wav" :AAFileType.music, + "wma" :AAFileType.music, + "aac" :AAFileType.music, - "doc" :AAFileType.Doc, - "docm" :AAFileType.Doc, - "dot" :AAFileType.Doc, - "dotx" :AAFileType.Doc, - "epub" :AAFileType.Doc, - "fb2" :AAFileType.Doc, - "xml" :AAFileType.Doc, - "info" :AAFileType.Doc, - "tex" :AAFileType.Doc, - "stw" :AAFileType.Doc, - "sxw" :AAFileType.Doc, - "txt" :AAFileType.Doc, - "xlc" :AAFileType.Doc, - "odf" :AAFileType.Doc, - "odt" :AAFileType.Doc, - "ott" :AAFileType.Doc, - "rtf" :AAFileType.Doc, - "pages":AAFileType.Doc, - "ini" :AAFileType.Doc, + "doc" :AAFileType.doc, + "docm" :AAFileType.doc, + "dot" :AAFileType.doc, + "dotx" :AAFileType.doc, + "epub" :AAFileType.doc, + "fb2" :AAFileType.doc, + "xml" :AAFileType.doc, + "info" :AAFileType.doc, + "tex" :AAFileType.doc, + "stw" :AAFileType.doc, + "sxw" :AAFileType.doc, + "txt" :AAFileType.doc, + "xlc" :AAFileType.doc, + "odf" :AAFileType.doc, + "odt" :AAFileType.doc, + "ott" :AAFileType.doc, + "rtf" :AAFileType.doc, + "pages":AAFileType.doc, + "ini" :AAFileType.doc, - "xls" :AAFileType.Spreadsheet, - "xlsx" :AAFileType.Spreadsheet, - "xlsm" :AAFileType.Spreadsheet, - "xlsb" :AAFileType.Spreadsheet, - "numbers":AAFileType.Spreadsheet, + "xls" :AAFileType.spreadsheet, + "xlsx" :AAFileType.spreadsheet, + "xlsm" :AAFileType.spreadsheet, + "xlsb" :AAFileType.spreadsheet, + "numbers":AAFileType.spreadsheet, - "jpg" :AAFileType.Picture, - "jpeg" :AAFileType.Picture, - "jp2" :AAFileType.Picture, - "jps" :AAFileType.Picture, - "gif" :AAFileType.Picture, - "tiff" :AAFileType.Picture, - "png" :AAFileType.Picture, - "psd" :AAFileType.Picture, - "webp" :AAFileType.Picture, - "ico" :AAFileType.Picture, - "pcx" :AAFileType.Picture, - "tga" :AAFileType.Picture, - "raw" :AAFileType.Picture, - "svg" :AAFileType.Picture, + "jpg" :AAFileType.picture, + "jpeg" :AAFileType.picture, + "jp2" :AAFileType.picture, + "jps" :AAFileType.picture, + "gif" :AAFileType.picture, + "tiff" :AAFileType.picture, + "png" :AAFileType.picture, + "psd" :AAFileType.picture, + "webp" :AAFileType.picture, + "ico" :AAFileType.picture, + "pcx" :AAFileType.picture, + "tga" :AAFileType.picture, + "raw" :AAFileType.picture, + "svg" :AAFileType.picture, - "mp4" :AAFileType.Video, - "3gp" :AAFileType.Video, - "m4v" :AAFileType.Video, - "webm" :AAFileType.Video, + "mp4" :AAFileType.video, + "3gp" :AAFileType.video, + "m4v" :AAFileType.video, + "webm" :AAFileType.video, - "ppt" :AAFileType.Presentation, - "key" :AAFileType.Presentation, - "keynote" :AAFileType.Presentation, + "ppt" :AAFileType.presentation, + "key" :AAFileType.presentation, + "keynote" :AAFileType.presentation, - "pdf" :AAFileType.PDF, - "apk" :AAFileType.APK, - "rar" :AAFileType.RAR, - "zip" :AAFileType.ZIP, - "csv" :AAFileType.CSV, + "pdf" :AAFileType.pdf, + "apk" :AAFileType.apk, + "rar" :AAFileType.rar, + "zip" :AAFileType.zip, + "csv" :AAFileType.csv, - "xhtm" :AAFileType.HTML, - "htm" :AAFileType.HTML, - "html" :AAFileType.HTML, + "xhtm" :AAFileType.html, + "htm" :AAFileType.html, + "html" :AAFileType.html, ] enum AAFileType { - case Music - case Doc - case Spreadsheet - case Picture - case Video - case Presentation - case PDF - case APK - case RAR - case ZIP - case CSV - case HTML - case UNKNOWN -} \ No newline at end of file + case music + case doc + case spreadsheet + case picture + case video + case presentation + case pdf + case apk + case rar + case zip + case csv + case html + case unknown +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AAHashMap.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AAHashMap.swift index 26f7f4b414..741e3be126 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AAHashMap.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AAHashMap.swift @@ -12,7 +12,7 @@ struct AAHashMap { } } - mutating func setKey(key: Int64, withValue val: T?) { + mutating func setKey(_ key: Int64, withValue val: T?) { let hashedString = Int(abs(key) % 10) if let collisionList = table[hashedString] { collisionList.upsertNodeWithKey(key, AndValue: val) @@ -21,7 +21,7 @@ struct AAHashMap { table[hashedString]!.upsertNodeWithKey(key, AndValue: val) } } - func getValueAtKey(key: Int64) -> T? { + func getValueAtKey(_ key: Int64) -> T? { let hashedString = Int(abs(key) % 10) if let collisionList = table[hashedString] { return collisionList.findNodeWithKey(key)?.value @@ -34,7 +34,7 @@ struct AAHashMap { struct SinglyLinkedList { var head = CCHeadNode>() - func findNodeWithKey(key: Int64) -> CCSinglyNode? { + func findNodeWithKey(_ key: Int64) -> CCSinglyNode? { var res: CCSinglyNode? autoreleasepool { if var currentNode = head.next { @@ -55,7 +55,7 @@ struct SinglyLinkedList { return res } - func upsertNodeWithKey(key: Int64, AndValue val: T?) -> CCSinglyNode { + func upsertNodeWithKey(_ key: Int64, AndValue val: T?) -> CCSinglyNode { if var currentNode = head.next { while let nextNode = currentNode.next { if currentNode.key == key { diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AASwiftlyLRU.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AASwiftlyLRU.swift index 3a235e6774..97d77fdd38 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AASwiftlyLRU.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AASwiftlyLRU.swift @@ -45,7 +45,7 @@ class LinkedList { } - func addToHead(node: Node) { + func addToHead(_ node: Node) { if self.head == nil { self.head = node self.tail = node @@ -58,7 +58,7 @@ class LinkedList { } } - func remove(node: Node) { + func remove(_ node: Node) { if node === self.head { if self.head?.next != nil { self.head = self.head?.next @@ -94,8 +94,8 @@ class AASwiftlyLRU : CustomStringConvertible { let capacity: Int var length = 0 - private let queue: LinkedList - private var hashtable: [K : Node] + fileprivate let queue: LinkedList + fileprivate var hashtable: [K : Node] /** Least Recently Used "LRU" Cache, capacity is the number of elements to keep in the Cache. @@ -134,7 +134,7 @@ class AASwiftlyLRU : CustomStringConvertible { self.length += 1 } else { - hashtable.removeValueForKey(self.queue.tail!.key) + hashtable.removeValue(forKey: self.queue.tail!.key) self.queue.tail = self.queue.tail?.previous if let node = self.queue.tail { @@ -151,4 +151,4 @@ class AASwiftlyLRU : CustomStringConvertible { var description : String { return "SwiftlyLRU Cache(\(self.length)) \n" + self.queue.display() } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AATools.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AATools.swift index 083e99456d..3619e33601 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AATools.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/AATools.swift @@ -7,39 +7,39 @@ import zipzap class AATools { - class func copyFileCommand(from: String, to: String) -> ACCommand { + class func copyFileCommand(_ from: String, to: String) -> ACCommand { return CopyCommand(from: from, to: to) } - class func zipDirectoryCommand(from: String, to: String) -> ACCommand { + class func zipDirectoryCommand(_ from: String, to: String) -> ACCommand { return ZipCommand(dir: from, to: to) } - class func isValidEmail(testStr:String) -> Bool { + class func isValidEmail(_ testStr:String) -> Bool { let emailRegEx = "^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}" let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx) - return emailTest.evaluateWithObject(testStr) + return emailTest.evaluate(with: testStr) } } private class ZipCommand: BackgroundCommand { - private let dir: String - private let to: String + fileprivate let dir: String + fileprivate let to: String init(dir: String, to: String) { self.dir = dir self.to = to } - private override func backgroundTask() throws { - let rootPath = NSURL(fileURLWithPath: dir).lastPathComponent! + fileprivate override func backgroundTask() throws { + let rootPath = URL(fileURLWithPath: dir).lastPathComponent - let zip = try ZZArchive(URL: NSURL(fileURLWithPath: to), options: [ZZOpenOptionsCreateIfMissingKey: true]) + let zip = try ZZArchive(url: URL(fileURLWithPath: to), options: [ZZOpenOptionsCreateIfMissingKey: true]) - let subs = try NSFileManager.defaultManager().subpathsOfDirectoryAtPath(dir) + let subs = try FileManager.default.subpathsOfDirectory(atPath: dir) var entries = [ZZArchiveEntry]() for p in subs { @@ -49,15 +49,15 @@ private class ZipCommand: BackgroundCommand { // Check path type: directory or file? var isDir : ObjCBool = false - if NSFileManager.defaultManager().fileExistsAtPath(fullPath, isDirectory: &isDir) { + if FileManager.default.fileExists(atPath: fullPath, isDirectory: &isDir) { - if !isDir { + if !isDir.boolValue { // If file write file - entries.append(ZZArchiveEntry(fileName: destPath, compress: false, dataBlock: { (error) -> NSData! in + entries.append(ZZArchiveEntry(fileName: destPath, compress: false, dataBlock: { (error) -> Data! in // TODO: Error handling? - return NSData(contentsOfFile: fullPath)! + return (try! Data(contentsOf: URL(fileURLWithPath: fullPath))) })) } else { @@ -86,15 +86,15 @@ private class CopyCommand: BackgroundCommand { self.to = to } - private override func backgroundTask() throws { - try NSFileManager.defaultManager().copyItemAtPath(from, toPath: to) + fileprivate override func backgroundTask() throws { + try FileManager.default.copyItem(atPath: from, toPath: to) } } class BackgroundCommand: NSObject, ACCommand { - func startWithCallback(callback: ACCommandCallback!) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { () -> Void in + func start(with callback: ACCommandCallback!) { + DispatchQueue.global(priority: DispatchQueue.GlobalQueuePriority.default).async { () -> Void in do { try self.backgroundTask() callback.onResult(nil) @@ -107,4 +107,4 @@ class BackgroundCommand: NSObject, ACCommand { func backgroundTask() throws { } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Bundle.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Bundle.swift index fa12f51dab..a1513ef7d0 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Bundle.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Bundle.swift @@ -4,10 +4,10 @@ import Foundation -let frameworkBundle = NSBundle(identifier: "im.actor.ActorSDK")! +let frameworkBundle = Bundle(identifier: "im.actor.ActorSDK")! -public extension NSBundle { - static var framework: NSBundle { +public extension Bundle { + static var framework: Bundle { get { return frameworkBundle } @@ -15,11 +15,11 @@ public extension NSBundle { } public extension UIImage { - class func bundled(named: String) -> UIImage? { + class func bundled(_ named: String) -> UIImage? { if let appImage = UIImage(named: named) { return appImage } - return UIImage(named: named, inBundle: NSBundle.framework, compatibleWithTraitCollection: UITraitCollection(displayScale: UIScreen.mainScreen().scale)) + return UIImage(named: named, in: Bundle.framework, compatibleWith: UITraitCollection(displayScale: UIScreen.main.scale)) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Categories/AACustomPresentationAnimationController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Categories/AACustomPresentationAnimationController.swift index 0040936b81..bcc8135847 100755 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Categories/AACustomPresentationAnimationController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Categories/AACustomPresentationAnimationController.swift @@ -8,7 +8,7 @@ import UIKit class AACustomPresentationAnimationController: NSObject, UIViewControllerAnimatedTransitioning { let isPresenting :Bool - let duration :NSTimeInterval = 0.5 + let duration :TimeInterval = 0.5 init(isPresenting: Bool) { self.isPresenting = isPresenting @@ -19,11 +19,11 @@ class AACustomPresentationAnimationController: NSObject, UIViewControllerAnimate // ---- UIViewControllerAnimatedTransitioning methods - func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval { + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return self.duration } - func animateTransition(transitionContext: UIViewControllerContextTransitioning) { + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { if isPresenting { animatePresentationWithTransitionContext(transitionContext) } @@ -35,31 +35,31 @@ class AACustomPresentationAnimationController: NSObject, UIViewControllerAnimate // ---- Helper methods - func animatePresentationWithTransitionContext(transitionContext: UIViewControllerContextTransitioning) { - let presentedController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)! - let presentedControllerView = transitionContext.viewForKey(UITransitionContextToViewKey)! - let containerView = transitionContext.containerView() + func animatePresentationWithTransitionContext(_ transitionContext: UIViewControllerContextTransitioning) { + let presentedController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)! + let presentedControllerView = transitionContext.view(forKey: UITransitionContextViewKey.to)! + let containerView = transitionContext.containerView // Position the presented view off the top of the container view - presentedControllerView.frame = transitionContext.finalFrameForViewController(presentedController) + presentedControllerView.frame = transitionContext.finalFrame(for: presentedController) //presentedControllerView.center.y -= containerView!.bounds.size.height - containerView!.addSubview(presentedControllerView) + containerView.addSubview(presentedControllerView) // Animate the presented view to it's final position - UIView.animateWithDuration(self.duration, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.0, options: .AllowUserInteraction, animations: { + UIView.animate(withDuration: self.duration, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.0, options: .allowUserInteraction, animations: { //presentedControllerView.center.y += containerView!.bounds.size.height }, completion: {(completed: Bool) -> Void in transitionContext.completeTransition(completed) }) } - func animateDismissalWithTransitionContext(transitionContext: UIViewControllerContextTransitioning) { - let presentedControllerView = transitionContext.viewForKey(UITransitionContextFromViewKey)! + func animateDismissalWithTransitionContext(_ transitionContext: UIViewControllerContextTransitioning) { + let presentedControllerView = transitionContext.view(forKey: UITransitionContextViewKey.from)! //let containerView = transitionContext.containerView() // Animate the presented view off the bottom of the view - UIView.animateWithDuration(self.duration, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.0, options: .AllowUserInteraction, animations: { + UIView.animate(withDuration: self.duration, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.0, options: .allowUserInteraction, animations: { //presentedControllerView.center.y += containerView!.bounds.size.height presentedControllerView.alpha = 0.0 }, completion: {(completed: Bool) -> Void in diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Categories/AACustomPresentationController.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Categories/AACustomPresentationController.swift index 4c243b1b04..50589261e1 100755 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Categories/AACustomPresentationController.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Categories/AACustomPresentationController.swift @@ -18,17 +18,17 @@ class AACustomPresentationController: UIPresentationController { // Add the dimming view and the presented view to the heirarchy self.dimmingView.frame = self.containerView!.bounds self.containerView!.addSubview(self.dimmingView) - self.containerView!.addSubview(self.presentedView()!) + self.containerView!.addSubview(self.presentedView!) // Fade in the dimming view alongside the transition - if let transitionCoordinator = self.presentingViewController.transitionCoordinator() { - transitionCoordinator.animateAlongsideTransition({(context: UIViewControllerTransitionCoordinatorContext!) -> Void in + if let transitionCoordinator = self.presentingViewController.transitionCoordinator { + transitionCoordinator.animate(alongsideTransition: {(context: UIViewControllerTransitionCoordinatorContext!) -> Void in self.dimmingView.alpha = 1.0 }, completion:nil) } } - override func presentationTransitionDidEnd(completed: Bool) { + override func presentationTransitionDidEnd(_ completed: Bool) { // If the presentation didn't complete, remove the dimming view if !completed { self.dimmingView.removeFromSuperview() @@ -37,24 +37,24 @@ class AACustomPresentationController: UIPresentationController { override func dismissalTransitionWillBegin() { // Fade out the dimming view alongside the transition - if let transitionCoordinator = self.presentingViewController.transitionCoordinator() { - transitionCoordinator.animateAlongsideTransition({(context: UIViewControllerTransitionCoordinatorContext!) -> Void in + if let transitionCoordinator = self.presentingViewController.transitionCoordinator { + transitionCoordinator.animate(alongsideTransition: {(context: UIViewControllerTransitionCoordinatorContext!) -> Void in self.dimmingView.alpha = 0.0 }, completion:nil) } } - override func dismissalTransitionDidEnd(completed: Bool) { + override func dismissalTransitionDidEnd(_ completed: Bool) { // If the dismissal completed, remove the dimming view if completed { self.dimmingView.removeFromSuperview() } } - override func frameOfPresentedViewInContainerView() -> CGRect { + override var frameOfPresentedViewInContainerView : CGRect { // We don't want the presented view to fill the whole container view, so inset it's frame var frame = self.containerView!.bounds; - frame = CGRectInset(frame, 0.0, 0.0) + frame = frame.insetBy(dx: 0.0, dy: 0.0) return frame } @@ -62,10 +62,10 @@ class AACustomPresentationController: UIPresentationController { // ---- UIContentContainer protocol methods - override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator transitionCoordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransitionToSize(size, withTransitionCoordinator: transitionCoordinator) + override func viewWillTransition(to size: CGSize, with transitionCoordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: transitionCoordinator) - transitionCoordinator.animateAlongsideTransition({(context: UIViewControllerTransitionCoordinatorContext!) -> Void in + transitionCoordinator.animate(alongsideTransition: {(context: UIViewControllerTransitionCoordinatorContext!) -> Void in self.dimmingView.frame = self.containerView!.bounds }, completion:nil) } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Extensions/AttributedLabel.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Extensions/AttributedLabel.swift index 3941996292..b4e8e3ea55 100755 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Extensions/AttributedLabel.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Extensions/AttributedLabel.swift @@ -1,82 +1,82 @@ import UIKit -public class AttributedLabel: UIView { +open class AttributedLabel: UIView { public enum ContentAlignment: Int { - case Center - case Top - case Bottom - case Left - case Right - case TopLeft - case TopRight - case BottomLeft - case BottomRight + case center + case top + case bottom + case left + case right + case topLeft + case topRight + case bottomLeft + case bottomRight - func alignOffset(viewSize viewSize: CGSize, containerSize: CGSize) -> CGPoint { + func alignOffset(viewSize: CGSize, containerSize: CGSize) -> CGPoint { let xMargin = viewSize.width - containerSize.width let yMargin = viewSize.height - containerSize.height switch self { - case Center: + case .center: return CGPoint(x: max(xMargin / 2, 0), y: max(yMargin / 2, 0)) - case Top: + case .top: return CGPoint(x: max(xMargin / 2, 0), y: 0) - case Bottom: + case .bottom: return CGPoint(x: max(xMargin / 2, 0), y: max(yMargin, 0)) - case Left: + case .left: return CGPoint(x: 0, y: max(yMargin / 2, 0)) - case Right: + case .right: return CGPoint(x: max(xMargin, 0), y: max(yMargin / 2, 0)) - case TopLeft: + case .topLeft: return CGPoint(x: 0, y: 0) - case TopRight: + case .topRight: return CGPoint(x: max(xMargin, 0), y: 0) - case BottomLeft: + case .bottomLeft: return CGPoint(x: 0, y: max(yMargin, 0)) - case BottomRight: + case .bottomRight: return CGPoint(x: max(xMargin, 0), y: max(yMargin, 0)) } } } /// default is `0`. - public var numberOfLines: Int = 0 { + open var numberOfLines: Int = 0 { didSet { setNeedsDisplay() } } /// default is `Left`. - public var contentAlignment: ContentAlignment = .Left { + open var contentAlignment: ContentAlignment = .left { didSet { setNeedsDisplay() } } /// `lineFragmentPadding` of `NSTextContainer`. default is `0`. - public var padding: CGFloat = 0 { + open var padding: CGFloat = 0 { didSet { setNeedsDisplay() } } /// default is system font 17 plain. - public var font = UIFont.systemFontOfSize(17) { + open var font = UIFont.systemFont(ofSize: 17) { didSet { setNeedsDisplay() } } /// default is `ByTruncatingTail`. - public var lineBreakMode: NSLineBreakMode = .ByTruncatingTail { + open var lineBreakMode: NSLineBreakMode = .byTruncatingTail { didSet { setNeedsDisplay() } } /// default is nil (text draws black). - public var textColor: UIColor? { + open var textColor: UIColor? { didSet { setNeedsDisplay() } } /// default is nil. - public var paragraphStyle: NSParagraphStyle? { + open var paragraphStyle: NSParagraphStyle? { didSet { setNeedsDisplay() } } /// default is nil. - public var shadow: NSShadow? { + open var shadow: NSShadow? { didSet { setNeedsDisplay() } } /// default is nil. - public var attributedText: NSAttributedString? { + open var attributedText: NSAttributedString? { didSet { setNeedsDisplay() } } /// default is nil. - public var text: String? { + open var text: String? { get { return attributedText?.string } @@ -89,7 +89,7 @@ public class AttributedLabel: UIView { } } - private var mergedAttributedText: NSAttributedString? { + fileprivate var mergedAttributedText: NSAttributedString? { if let attributedText = attributedText { return mergeAttributes(attributedText) } @@ -99,24 +99,24 @@ public class AttributedLabel: UIView { public override init(frame: CGRect) { super.init(frame: frame) - opaque = false - contentMode = .Redraw + isOpaque = false + contentMode = .redraw } public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) - opaque = false - contentMode = .Redraw + isOpaque = false + contentMode = .redraw } - public override func setNeedsDisplay() { - if NSThread.isMainThread() { + open override func setNeedsDisplay() { + if Thread.isMainThread { super.setNeedsDisplay() } } - public override func drawRect(rect: CGRect) { + open override func draw(_ rect: CGRect) { guard let attributedText = mergedAttributedText else { return } @@ -127,15 +127,15 @@ public class AttributedLabel: UIView { let storage = NSTextStorage(attributedString: attributedText) storage.addLayoutManager(manager) - let frame = manager.usedRectForTextContainer(container) - let point = contentAlignment.alignOffset(viewSize: rect.size, containerSize: CGRectIntegral(frame).size) + let frame = manager.usedRect(for: container) + let point = contentAlignment.alignOffset(viewSize: rect.size, containerSize: frame.integral.size) - let glyphRange = manager.glyphRangeForTextContainer(container) - manager.drawBackgroundForGlyphRange(glyphRange, atPoint: point) - manager.drawGlyphsForGlyphRange(glyphRange, atPoint: point) + let glyphRange = manager.glyphRange(for: container) + manager.drawBackground(forGlyphRange: glyphRange, at: point) + manager.drawGlyphs(forGlyphRange: glyphRange, at: point) } - public override func sizeThatFits(size: CGSize) -> CGSize { + open override func sizeThatFits(_ size: CGSize) -> CGSize { guard let attributedText = mergedAttributedText else { return super.sizeThatFits(size) } @@ -146,17 +146,17 @@ public class AttributedLabel: UIView { let storage = NSTextStorage(attributedString: attributedText) storage.addLayoutManager(manager) - let frame = manager.usedRectForTextContainer(container) - return CGRectIntegral(frame).size + let frame = manager.usedRect(for: container) + return frame.integral.size } - public override func sizeToFit() { + open override func sizeToFit() { super.sizeToFit() - frame.size = sizeThatFits(CGSize(width: bounds.width, height: CGFloat.max)) + frame.size = sizeThatFits(CGSize(width: bounds.width, height: CGFloat.greatestFiniteMagnitude)) } - private func textContainer(size: CGSize) -> NSTextContainer { + fileprivate func textContainer(_ size: CGSize) -> NSTextContainer { let container = NSTextContainer(size: size) container.lineBreakMode = lineBreakMode container.lineFragmentPadding = padding @@ -164,13 +164,13 @@ public class AttributedLabel: UIView { return container } - private func layoutManager(container: NSTextContainer) -> NSLayoutManager { + fileprivate func layoutManager(_ container: NSTextContainer) -> NSLayoutManager { let layoutManager = NSLayoutManager() layoutManager.addTextContainer(container) return layoutManager } - private func mergeAttributes(attributedText: NSAttributedString) -> NSAttributedString { + fileprivate func mergeAttributes(_ attributedText: NSAttributedString) -> NSAttributedString { let attrString = NSMutableAttributedString(attributedString: attributedText) addAttribute(attrString, attrName: NSFontAttributeName, attr: font) @@ -190,9 +190,9 @@ public class AttributedLabel: UIView { return attrString } - private func addAttribute(attrString: NSMutableAttributedString, attrName: String, attr: AnyObject) { + fileprivate func addAttribute(_ attrString: NSMutableAttributedString, attrName: String, attr: AnyObject) { let range = NSRange(location: 0, length: attrString.length) - attrString.enumerateAttribute(attrName, inRange: range, options: .Reverse) { object, range, pointer in + attrString.enumerateAttribute(attrName, in: range, options: .reverse) { object, range, pointer in if object == nil { attrString.addAttributes([attrName: attr], range: range) } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Extensions/TapedLabel/AATapLabel.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Extensions/TapedLabel/AATapLabel.swift index cea3be4c8d..3a617b4151 100755 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Extensions/TapedLabel/AATapLabel.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Extensions/TapedLabel/AATapLabel.swift @@ -4,34 +4,34 @@ import Foundation -public class TapLabel: UILabel, NSLayoutManagerDelegate { +open class TapLabel: UILabel, NSLayoutManagerDelegate { - public static let LinkContentName = "TapLabelLinkContentName" - public static let SelectedForegroudColorName = "TapLabelSelectedForegroudColorName" + open static let LinkContentName = "TapLabelLinkContentName" + open static let SelectedForegroudColorName = "TapLabelSelectedForegroudColorName" - public weak var delegate: AATapLabelDelegate? + open weak var delegate: AATapLabelDelegate? - private let layoutManager = NSLayoutManager() - private let textContainer = NSTextContainer() - private let textStorage = NSTextStorage() - private var rangesForUrls = [NSRange]() - private var links = [String: NSRange]() - private var isTouchMoved = false - private var defaultSelectedForegroundColor: UIColor? + fileprivate let layoutManager = NSLayoutManager() + fileprivate let textContainer = NSTextContainer() + fileprivate let textStorage = NSTextStorage() + fileprivate var rangesForUrls = [NSRange]() + fileprivate var links = [String: NSRange]() + fileprivate var isTouchMoved = false + fileprivate var defaultSelectedForegroundColor: UIColor? - private var selected: (String, NSRange)? { + fileprivate var selected: (String, NSRange)? { didSet { if let (_, range) = selected { if let currentColor = textStorage.attribute(NSForegroundColorAttributeName, - atIndex: range.location, + at: range.location, effectiveRange: nil) as? UIColor { defaultSelectedForegroundColor = currentColor } if let color = textStorage.attribute(TapLabel.SelectedForegroudColorName, - atIndex: range.location, + at: range.location, effectiveRange: nil) as? UIColor { textStorage.addAttribute(NSForegroundColorAttributeName, value: color, range: range) @@ -48,19 +48,19 @@ public class TapLabel: UILabel, NSLayoutManagerDelegate { } } - public override var lineBreakMode: NSLineBreakMode { + open override var lineBreakMode: NSLineBreakMode { didSet { textContainer.lineBreakMode = lineBreakMode } } - public override var numberOfLines: Int { + open override var numberOfLines: Int { didSet { textContainer.maximumNumberOfLines = numberOfLines } } - public override var attributedText: NSAttributedString! { + open override var attributedText: NSAttributedString! { didSet { textStorage.setAttributedString(attributedText) updateLinks() @@ -68,13 +68,13 @@ public class TapLabel: UILabel, NSLayoutManagerDelegate { } } - public override var frame: CGRect { + open override var frame: CGRect { didSet { textContainer.size = frame.size } } - public override var bounds: CGRect { + open override var bounds: CGRect { didSet { textContainer.size = bounds.size } @@ -93,19 +93,19 @@ public class TapLabel: UILabel, NSLayoutManagerDelegate { textStorage.addLayoutManager(layoutManager) - userInteractionEnabled = true + isUserInteractionEnabled = true } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private func updateLinks() { + fileprivate func updateLinks() { links = [String: NSRange]() attributedText.enumerateAttribute(TapLabel.LinkContentName, - inRange: NSMakeRange(0, attributedText.length), - options: NSAttributedStringEnumerationOptions(rawValue: 0)) + in: NSMakeRange(0, attributedText.length), + options: NSAttributedString.EnumerationOptions(rawValue: 0)) { value, range, stop in @@ -115,7 +115,7 @@ public class TapLabel: UILabel, NSLayoutManagerDelegate { } } - private func updateRangesForUrls() + fileprivate func updateRangesForUrls() { // var error: NSError? @@ -136,7 +136,7 @@ public class TapLabel: UILabel, NSLayoutManagerDelegate { // rangesForUrls = matches.map { $0.range } } - public override func textRectForBounds(bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect + open override func textRect(forBounds bounds: CGRect, limitedToNumberOfLines numberOfLines: Int) -> CGRect { let savedTextContainerSize = textContainer.size let savedTextContainerNumberOfLines = textContainer.maximumNumberOfLines @@ -144,8 +144,8 @@ public class TapLabel: UILabel, NSLayoutManagerDelegate { textContainer.size = bounds.size textContainer.maximumNumberOfLines = numberOfLines - let glyphRange = layoutManager.glyphRangeForTextContainer(textContainer) - var textBounds = layoutManager.boundingRectForGlyphRange(glyphRange, inTextContainer:textContainer) + let glyphRange = layoutManager.glyphRange(for: textContainer) + var textBounds = layoutManager.boundingRect(forGlyphRange: glyphRange, in:textContainer) textBounds.origin = bounds.origin textBounds.size.width = ceil(textBounds.size.width) @@ -157,20 +157,20 @@ public class TapLabel: UILabel, NSLayoutManagerDelegate { return textBounds; } - public override func drawTextInRect(rect: CGRect) + open override func drawText(in rect: CGRect) { - let glyphRange = layoutManager.glyphRangeForTextContainer(textContainer) + let glyphRange = layoutManager.glyphRange(for: textContainer) let textOffset = calcTextOffsetForGlyphRange(glyphRange) - layoutManager.drawBackgroundForGlyphRange(glyphRange, atPoint:textOffset) - layoutManager.drawGlyphsForGlyphRange(glyphRange, atPoint:textOffset) + layoutManager.drawBackground(forGlyphRange: glyphRange, at:textOffset) + layoutManager.drawGlyphs(forGlyphRange: glyphRange, at:textOffset) } - private func calcTextOffsetForGlyphRange(glyphRange: NSRange) -> CGPoint + fileprivate func calcTextOffsetForGlyphRange(_ glyphRange: NSRange) -> CGPoint { - var textOffset = CGPointZero + var textOffset = CGPoint.zero - let textBounds = layoutManager.boundingRectForGlyphRange(glyphRange, inTextContainer:textContainer) + let textBounds = layoutManager.boundingRect(forGlyphRange: glyphRange, in:textContainer) let paddingHeight = (self.bounds.size.height - textBounds.size.height) / 2 if (paddingHeight > 0) { textOffset.y = paddingHeight; @@ -179,24 +179,24 @@ public class TapLabel: UILabel, NSLayoutManagerDelegate { return textOffset; } - private func linkAtPoint(point: CGPoint) -> (String, NSRange)? { + fileprivate func linkAtPoint(_ point: CGPoint) -> (String, NSRange)? { var point2 = point if textStorage.length == 0 { return nil } - let glyphRange = layoutManager.glyphRangeForTextContainer(textContainer) + let glyphRange = layoutManager.glyphRange(for: textContainer) let textOffset = calcTextOffsetForGlyphRange(glyphRange) point2.x = point2.x - textOffset.x point2.y = point2.y - textOffset.y - let touchedChar = layoutManager.glyphIndexForPoint(point2, inTextContainer:textContainer) + let touchedChar = layoutManager.glyphIndex(for: point2, in:textContainer) var lineRange = NSRange() - let lineRect = layoutManager.lineFragmentUsedRectForGlyphAtIndex(touchedChar, effectiveRange:&lineRange) + let lineRect = layoutManager.lineFragmentUsedRect(forGlyphAt: touchedChar, effectiveRange:&lineRange) - if !CGRectContainsPoint(lineRect, point2) { + if !lineRect.contains(point2) { return nil } @@ -212,27 +212,27 @@ public class TapLabel: UILabel, NSLayoutManagerDelegate { //MARK: - Interactions - public override func touchesBegan(touches: Set, withEvent event: UIEvent?) { + open override func touchesBegan(_ touches: Set, with event: UIEvent?) { isTouchMoved = false - let touchLocation = touches.first!.locationInView(self) + let touchLocation = touches.first!.location(in: self) if let (link, range) = linkAtPoint(touchLocation) { selected = (link, range) } else { - super.touchesBegan(touches, withEvent: event) + super.touchesBegan(touches, with: event) } } - public override func touchesMoved(touches: Set, withEvent event: UIEvent?) { - super.touchesMoved(touches, withEvent: event) + open override func touchesMoved(_ touches: Set, with event: UIEvent?) { + super.touchesMoved(touches, with: event) isTouchMoved = true selected = nil } - public override func touchesEnded(touches: Set, withEvent event: UIEvent?) { - super.touchesEnded(touches, withEvent: event) + open override func touchesEnded(_ touches: Set, with event: UIEvent?) { + super.touchesEnded(touches, with: event) if !isTouchMoved { if (selected != nil) { @@ -244,17 +244,17 @@ public class TapLabel: UILabel, NSLayoutManagerDelegate { } - public override func touchesCancelled(touches: Set?, withEvent event: UIEvent?) { - super.touchesCancelled(touches, withEvent: event) + open override func touchesCancelled(_ touches: Set, with event: UIEvent?) { + super.touchesCancelled(touches, with: event) selected = nil } //MARK: - NSLayoutManagerDelegate - @objc public func layoutManager( - layoutManager: NSLayoutManager, - shouldBreakLineByWordBeforeCharacterAtIndex charIndex: Int) -> Bool + @objc open func layoutManager( + _ layoutManager: NSLayoutManager, + shouldBreakLineByWordBeforeCharacterAt charIndex: Int) -> Bool { for range in rangesForUrls { if range.location < charIndex && charIndex < range.location + range.length { @@ -264,4 +264,4 @@ public class TapLabel: UILabel, NSLayoutManagerDelegate { return true } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Extensions/TapedLabel/AATapLabelDelegate.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Extensions/TapedLabel/AATapLabelDelegate.swift index a63c81d6c0..b00bb06c28 100755 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Extensions/TapedLabel/AATapLabelDelegate.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Extensions/TapedLabel/AATapLabelDelegate.swift @@ -6,6 +6,6 @@ import Foundation public protocol AATapLabelDelegate: class { - func tapLabel(tapLabel: TapLabel, didSelectLink link: String) + func tapLabel(_ tapLabel: TapLabel, didSelectLink link: String) -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Reachability.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Reachability.swift deleted file mode 100644 index a42432b8ae..0000000000 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Reachability.swift +++ /dev/null @@ -1,388 +0,0 @@ -/* -Copyright (c) 2014, Ashley Mills -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this -list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, -this list of conditions and the following disclaimer in the documentation -and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. -*/ - -import SystemConfiguration -import Foundation - -enum ReachabilityError: ErrorType { - case FailedToCreateWithAddress(sockaddr_in) - case FailedToCreateWithHostname(String) - case UnableToSetCallback - case UnableToSetDispatchQueue -} - -public let ReachabilityChangedNotification = "ReachabilityChangedNotification" - -func callback(reachability:SCNetworkReachability, flags: SCNetworkReachabilityFlags, info: UnsafeMutablePointer) { - let reachability = Unmanaged.fromOpaque(COpaquePointer(info)).takeUnretainedValue() - - dispatch_async(dispatch_get_main_queue()) { - reachability.reachabilityChanged(flags) - } -} - - -public class Reachability: NSObject { - - public typealias NetworkReachable = (Reachability) -> () - public typealias NetworkUnreachable = (Reachability) -> () - - public enum NetworkStatus: CustomStringConvertible { - - case NotReachable, ReachableViaWiFi, ReachableViaWWAN - - public var description: String { - switch self { - case .ReachableViaWWAN: - return "Cellular" - case .ReachableViaWiFi: - return "WiFi" - case .NotReachable: - return "No Connection" - } - } - } - - // MARK: - *** Public properties *** - - public var whenReachable: NetworkReachable? - public var whenUnreachable: NetworkUnreachable? - public var reachableOnWWAN: Bool - public var notificationCenter = NSNotificationCenter.defaultCenter() - - public var currentReachabilityStatus: NetworkStatus { - if isReachable() { - if isReachableViaWiFi() { - return .ReachableViaWiFi - } - if isRunningOnDevice { - return .ReachableViaWWAN - } - } - - return .NotReachable - } - - public var currentReachabilityString: String { - return "\(currentReachabilityStatus)" - } - - // MARK: - *** Initialisation methods *** - - required public init(reachabilityRef: SCNetworkReachability) { - reachableOnWWAN = true - self.reachabilityRef = reachabilityRef - } - - public convenience init(hostname: String) throws { - - let nodename = (hostname as NSString).UTF8String - guard let ref = SCNetworkReachabilityCreateWithName(nil, nodename) else { throw ReachabilityError.FailedToCreateWithHostname(hostname) } - - self.init(reachabilityRef: ref) - } - - public class func reachabilityForInternetConnection() throws -> Reachability { - - var zeroAddress = sockaddr_in() - zeroAddress.sin_len = UInt8(sizeofValue(zeroAddress)) - zeroAddress.sin_family = sa_family_t(AF_INET) - - guard let ref = withUnsafePointer(&zeroAddress, { - SCNetworkReachabilityCreateWithAddress(nil, UnsafePointer($0)) - }) else { throw ReachabilityError.FailedToCreateWithAddress(zeroAddress) } - - return Reachability(reachabilityRef: ref) - } - - public class func reachabilityForLocalWiFi() throws -> Reachability { - - var localWifiAddress: sockaddr_in = sockaddr_in(sin_len: __uint8_t(0), sin_family: sa_family_t(0), sin_port: in_port_t(0), sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0)) - localWifiAddress.sin_len = UInt8(sizeofValue(localWifiAddress)) - localWifiAddress.sin_family = sa_family_t(AF_INET) - - // IN_LINKLOCALNETNUM is defined in as 169.254.0.0 - let address: UInt32 = 0xA9FE0000 - localWifiAddress.sin_addr.s_addr = in_addr_t(address.bigEndian) - - guard let ref = withUnsafePointer(&localWifiAddress, { - SCNetworkReachabilityCreateWithAddress(nil, UnsafePointer($0)) - }) else { throw ReachabilityError.FailedToCreateWithAddress(localWifiAddress) } - - return Reachability(reachabilityRef: ref) - } - - // MARK: - *** Notifier methods *** - public func startNotifier() throws { - - if notifierRunning { return } - - var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil) - context.info = UnsafeMutablePointer(Unmanaged.passUnretained(self).toOpaque()) - - if !SCNetworkReachabilitySetCallback(reachabilityRef!, callback, &context) { - stopNotifier() - throw ReachabilityError.UnableToSetCallback - } - - if !SCNetworkReachabilitySetDispatchQueue(reachabilityRef!, reachabilitySerialQueue) { - stopNotifier() - throw ReachabilityError.UnableToSetDispatchQueue - } - - notifierRunning = true - } - - - public func stopNotifier() { - if let reachabilityRef = reachabilityRef { - SCNetworkReachabilitySetCallback(reachabilityRef, nil, nil) - SCNetworkReachabilitySetDispatchQueue(reachabilityRef, nil) - } - notifierRunning = false - } - - // MARK: - *** Connection test methods *** - public func isReachable() -> Bool { - return isReachableWithTest({ (flags: SCNetworkReachabilityFlags) -> (Bool) in - return self.isReachableWithFlags(flags) - }) - } - - public func isReachableViaWWAN() -> Bool { - - if isRunningOnDevice { - return isReachableWithTest() { flags -> Bool in - // Check we're REACHABLE - if self.isReachable(flags) { - - // Now, check we're on WWAN - if self.isOnWWAN(flags) { - return true - } - } - return false - } - } - return false - } - - public func isReachableViaWiFi() -> Bool { - - return isReachableWithTest() { flags -> Bool in - - // Check we're reachable - if self.isReachable(flags) { - - if self.isRunningOnDevice { - // Check we're NOT on WWAN - if self.isOnWWAN(flags) { - return false - } - } - return true - } - - return false - } - } - - // MARK: - *** Private methods *** - private var isRunningOnDevice: Bool = { - #if (arch(i386) || arch(x86_64)) && os(iOS) - return false - #else - return true - #endif - }() - - private var notifierRunning = false - private var reachabilityRef: SCNetworkReachability? - private let reachabilitySerialQueue = dispatch_queue_create("uk.co.ashleymills.reachability", DISPATCH_QUEUE_SERIAL) - - private func reachabilityChanged(flags: SCNetworkReachabilityFlags) { - if isReachableWithFlags(flags) { - if let block = whenReachable { - block(self) - } - } else { - if let block = whenUnreachable { - block(self) - } - } - - notificationCenter.postNotificationName(ReachabilityChangedNotification, object:self) - } - - private func isReachableWithFlags(flags: SCNetworkReachabilityFlags) -> Bool { - - let reachable = isReachable(flags) - - if !reachable { - return false - } - - if isConnectionRequiredOrTransient(flags) { - return false - } - - if isRunningOnDevice { - if isOnWWAN(flags) && !reachableOnWWAN { - // We don't want to connect when on 3G. - return false - } - } - - return true - } - - private func isReachableWithTest(test: (SCNetworkReachabilityFlags) -> (Bool)) -> Bool { - - if let reachabilityRef = reachabilityRef { - - var flags = SCNetworkReachabilityFlags(rawValue: 0) - let gotFlags = withUnsafeMutablePointer(&flags) { - SCNetworkReachabilityGetFlags(reachabilityRef, UnsafeMutablePointer($0)) - } - - if gotFlags { - return test(flags) - } - } - - return false - } - - // WWAN may be available, but not active until a connection has been established. - // WiFi may require a connection for VPN on Demand. - private func isConnectionRequired() -> Bool { - return connectionRequired() - } - - private func connectionRequired() -> Bool { - return isReachableWithTest({ (flags: SCNetworkReachabilityFlags) -> (Bool) in - return self.isConnectionRequired(flags) - }) - } - - // Dynamic, on demand connection? - private func isConnectionOnDemand() -> Bool { - return isReachableWithTest({ (flags: SCNetworkReachabilityFlags) -> (Bool) in - return self.isConnectionRequired(flags) && self.isConnectionOnTrafficOrDemand(flags) - }) - } - - // Is user intervention required? - private func isInterventionRequired() -> Bool { - return isReachableWithTest({ (flags: SCNetworkReachabilityFlags) -> (Bool) in - return self.isConnectionRequired(flags) && self.isInterventionRequired(flags) - }) - } - - private func isOnWWAN(flags: SCNetworkReachabilityFlags) -> Bool { - #if os(iOS) - return flags.contains(.IsWWAN) - #else - return false - #endif - } - private func isReachable(flags: SCNetworkReachabilityFlags) -> Bool { - return flags.contains(.Reachable) - } - private func isConnectionRequired(flags: SCNetworkReachabilityFlags) -> Bool { - return flags.contains(.ConnectionRequired) - } - private func isInterventionRequired(flags: SCNetworkReachabilityFlags) -> Bool { - return flags.contains(.InterventionRequired) - } - private func isConnectionOnTraffic(flags: SCNetworkReachabilityFlags) -> Bool { - return flags.contains(.ConnectionOnTraffic) - } - private func isConnectionOnDemand(flags: SCNetworkReachabilityFlags) -> Bool { - return flags.contains(.ConnectionOnDemand) - } - func isConnectionOnTrafficOrDemand(flags: SCNetworkReachabilityFlags) -> Bool { - return !flags.intersect([.ConnectionOnTraffic, .ConnectionOnDemand]).isEmpty - } - private func isTransientConnection(flags: SCNetworkReachabilityFlags) -> Bool { - return flags.contains(.TransientConnection) - } - private func isLocalAddress(flags: SCNetworkReachabilityFlags) -> Bool { - return flags.contains(.IsLocalAddress) - } - private func isDirect(flags: SCNetworkReachabilityFlags) -> Bool { - return flags.contains(.IsDirect) - } - private func isConnectionRequiredOrTransient(flags: SCNetworkReachabilityFlags) -> Bool { - let testcase:SCNetworkReachabilityFlags = [.ConnectionRequired, .TransientConnection] - return flags.intersect(testcase) == testcase - } - - private var reachabilityFlags: SCNetworkReachabilityFlags { - if let reachabilityRef = reachabilityRef { - - var flags = SCNetworkReachabilityFlags(rawValue: 0) - let gotFlags = withUnsafeMutablePointer(&flags) { - SCNetworkReachabilityGetFlags(reachabilityRef, UnsafeMutablePointer($0)) - } - - if gotFlags { - return flags - } - } - - return [] - } - - override public var description: String { - - var W: String - if isRunningOnDevice { - W = isOnWWAN(reachabilityFlags) ? "W" : "-" - } else { - W = "X" - } - let R = isReachable(reachabilityFlags) ? "R" : "-" - let c = isConnectionRequired(reachabilityFlags) ? "c" : "-" - let t = isTransientConnection(reachabilityFlags) ? "t" : "-" - let i = isInterventionRequired(reachabilityFlags) ? "i" : "-" - let C = isConnectionOnTraffic(reachabilityFlags) ? "C" : "-" - let D = isConnectionOnDemand(reachabilityFlags) ? "D" : "-" - let l = isLocalAddress(reachabilityFlags) ? "l" : "-" - let d = isDirect(reachabilityFlags) ? "d" : "-" - - return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)" - } - - deinit { - stopNotifier() - - reachabilityRef = nil - whenReachable = nil - whenUnreachable = nil - } -} \ No newline at end of file diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Telephony.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Telephony.swift index ade125420c..c3894ece82 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Telephony.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Utils/Telephony.swift @@ -11,7 +11,7 @@ class AATelephony { let networkInfo = CTTelephonyNetworkInfo() let carrier = networkInfo.subscriberCellularProvider - let countryCode = carrier?.isoCountryCode?.lowercaseString + let countryCode = carrier?.isoCountryCode?.lowercased() if countryCode == nil { return "en" @@ -20,9 +20,9 @@ class AATelephony { } } - static func getCountry(iso: String) -> CountryDesc { + static func getCountry(_ iso: String) -> CountryDesc { for i in AATelephony.countryCodes { - if i.iso.lowercaseString == iso.lowercaseString { + if i.iso.lowercased() == iso.lowercased() { return i } } @@ -276,4 +276,4 @@ class CountryDesc { self.iso = iso self.country = country } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAAvatarView.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAAvatarView.swift index 5d98d5d971..a1b349b4e3 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAAvatarView.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAAvatarView.swift @@ -5,24 +5,24 @@ import UIKit import YYImage -public class AAAvatarView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { +open class AAAvatarView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { - private var title: String? - private var id: Int? - private var file: ACFileReference? - private var fileName: String? - private var showPlaceholder: Bool = false + fileprivate var title: String? + fileprivate var id: Int? + fileprivate var file: ACFileReference? + fileprivate var fileName: String? + fileprivate var showPlaceholder: Bool = false public init() { - super.init(frame: CGRectZero) + super.init(frame: CGRect.zero) self.layer.delegate = self - self.layer.contentsScale = UIScreen.mainScreen().scale - self.backgroundColor = UIColor.clearColor() - self.opaque = false - self.contentMode = .Redraw; + self.layer.contentsScale = UIScreen.main.scale + self.backgroundColor = UIColor.clear + self.isOpaque = false + self.contentMode = .redraw; if Actor.isLoggedIn() { - Actor.subscribeToDownloads(self) + Actor.subscribe(toDownloads: self) } } @@ -37,7 +37,7 @@ public class AAAvatarView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { } } - public func onDownloadedWithLong(fileId: jlong) { + open func onDownloaded(withLong fileId: jlong) { if self.file?.getFileId() == fileId { dispatchOnUi { if self.file?.getFileId() == fileId { @@ -51,7 +51,7 @@ public class AAAvatarView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { // Databinding // - public func bind(title: String, id: Int, fileName: String?) { + open func bind(_ title: String, id: Int, fileName: String?) { self.title = title self.id = id @@ -63,7 +63,7 @@ public class AAAvatarView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { self.layer.setNeedsDisplay() } - public func bind(title: String, id: Int, avatar: ACAvatar?) { + open func bind(_ title: String, id: Int, avatar: ACAvatar?) { if self.title == title && self.id == id @@ -87,7 +87,7 @@ public class AAAvatarView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { self.layer.setNeedsDisplay() } - public func unbind() { + open func unbind() { self.title = nil self.id = nil @@ -98,11 +98,11 @@ public class AAAvatarView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { self.layer.setNeedsDisplay() } - public override class func layerClass() -> AnyClass { + open override class var layerClass : AnyClass { return YYAsyncLayer.self } - public func newAsyncDisplayTask() -> YYAsyncLayerDisplayTask { + open func newAsyncDisplayTask() -> YYAsyncLayerDisplayTask { let res = YYAsyncLayerDisplayTask() let _id = id @@ -118,7 +118,7 @@ public class AAAvatarView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { if _fileName != nil { filePath = _fileName } else if _file != nil { - let desc = Actor.findDownloadedDescriptorWithFileId(_file!.getFileId()) + let desc = Actor.findDownloadedDescriptor(withFileId: _file!.getFileId()) if isCancelled() { return } @@ -126,7 +126,7 @@ public class AAAvatarView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { filePath = CocoaFiles.pathFromDescriptor(desc!) } else { // Request if not available - Actor.startDownloadingWithReference(_file!) + Actor.startDownloading(with: _file!) filePath = nil } } else { @@ -141,19 +141,19 @@ public class AAAvatarView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { if filePath == nil && _showPlaceholder && _id != nil && _title != nil { let colors = ActorSDK.sharedActor().style.avatarColors - let color = colors[_id! % colors.count].CGColor + let color = colors[_id! % colors.count].cgColor // Background - CGContextSetFillColorWithColor(context, color) + context.setFillColor(color) - CGContextAddArc(context, r, r, r, CGFloat(M_PI * 0), CGFloat(M_PI * 2), 0) + context.addEllipse(in: CGRect(x: 0, y: 0, width: r * 2, height: r * 2)) if isCancelled() { return } - CGContextDrawPath(context, .Fill) + context.drawPath(using: .fill) if isCancelled() { return @@ -161,23 +161,23 @@ public class AAAvatarView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { // Text - UIColor.whiteColor().set() + UIColor.white.set() if isCancelled() { return } - let font = UIFont.systemFontOfSize(r) - var rect = CGRectMake(0, 0, r * 2, r * 2) + let font = UIFont.systemFont(ofSize: r) + var rect = CGRect(x: 0, y: 0, width: r * 2, height: r * 2) rect.origin.y = round(CGFloat(r * 2 * 47 / 100) - font.pointSize / 2) - let style : NSMutableParagraphStyle = NSParagraphStyle.defaultParagraphStyle().mutableCopy() as! NSMutableParagraphStyle - style.alignment = NSTextAlignment.Center - style.lineBreakMode = NSLineBreakMode.ByWordWrapping + let style : NSMutableParagraphStyle = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle + style.alignment = NSTextAlignment.center + style.lineBreakMode = NSLineBreakMode.byWordWrapping let short = _title!.trim().smallValue() - short.drawInRect(rect, withAttributes: [NSParagraphStyleAttributeName:style, NSFontAttributeName:font, + short.draw(in: rect, withAttributes: [NSParagraphStyleAttributeName:style, NSFontAttributeName:font, NSForegroundColorAttributeName:ActorSDK.sharedActor().style.avatarTextColor]) if isCancelled() { @@ -194,25 +194,25 @@ public class AAAvatarView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { if image != nil { // Background - UIBezierPath(roundedRect: CGRectMake(0, 0, r * 2, r * 2), cornerRadius: r).addClip() + UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: r * 2, height: r * 2), cornerRadius: r).addClip() if isCancelled() { return } - image!.drawInRect(CGRectMake(0, 0, r * 2, r * 2)) + image!.draw(in: CGRect(x: 0, y: 0, width: r * 2, height: r * 2)) } else { // Clean BG - CGContextSetFillColorWithColor(context, UIColor.whiteColor().CGColor) + context.setFillColor(UIColor.white.cgColor) - CGContextAddArc(context, r, r, r, CGFloat(M_PI * 0), CGFloat(M_PI * 2), 0) + context.addEllipse(in: CGRect(x: 0, y: 0, width: r * 2, height: r * 2)) if isCancelled() { return } - CGContextDrawPath(context, .Fill) + context.drawPath(using: .fill) } if isCancelled() { @@ -220,19 +220,19 @@ public class AAAvatarView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { } } else { // Clean BG - CGContextSetFillColorWithColor(context, UIColor.whiteColor().CGColor) + context.setFillColor(UIColor.white.cgColor) if isCancelled() { return } - CGContextAddArc(context, r, r, r, CGFloat(M_PI * 0), CGFloat(M_PI * 2), 0) + context.addEllipse(in: CGRect(x: 0, y: 0, width: r * 2, height: r * 2)) if isCancelled() { return } - CGContextDrawPath(context, .Fill) + context.drawPath(using: .fill) if isCancelled() { return @@ -241,19 +241,19 @@ public class AAAvatarView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { // Border - CGContextSetStrokeColorWithColor(context, UIColor(red: 0, green: 0, blue: 0, alpha: 0x10/255.0).CGColor) + context.setStrokeColor(UIColor(red: 0, green: 0, blue: 0, alpha: 0x10/255.0).cgColor) if isCancelled() { return } - CGContextAddArc(context, r, r, r, CGFloat(M_PI * 0), CGFloat(M_PI * 2), 0) + context.addEllipse(in: CGRect(x: 0, y: 0, width: r * 2, height: r * 2)) if isCancelled() { return } - CGContextDrawPath(context, .Stroke) + context.drawPath(using: .stroke) } return res } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AABigPlaceholderView.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AABigPlaceholderView.swift index eb093eaf77..6b931ce7d4 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AABigPlaceholderView.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AABigPlaceholderView.swift @@ -6,33 +6,33 @@ import UIKit class AABigPlaceholderView: UIView { - private var contentView: UIView! - private var bgView: UIView! - private var imageView: UIImageView! - private var titleLabel: UILabel! - private var subtitleLabel: UILabel! - private var actionButton: UIButton! - private var topOffset: CGFloat! - private var subtitle2Label: UILabel! - private var action2Button: UIButton! + fileprivate var contentView: UIView! + fileprivate var bgView: UIView! + fileprivate var imageView: UIImageView! + fileprivate var titleLabel: UILabel! + fileprivate var subtitleLabel: UILabel! + fileprivate var actionButton: UIButton! + fileprivate var topOffset: CGFloat! + fileprivate var subtitle2Label: UILabel! + fileprivate var action2Button: UIButton! // // Constructors // init(topOffset: CGFloat!) { - super.init(frame: CGRectZero) + super.init(frame: CGRect.zero) self.topOffset = topOffset - backgroundColor = UIColor.whiteColor() + backgroundColor = UIColor.white contentView = UIView() - contentView.backgroundColor = UIColor.whiteColor() + contentView.backgroundColor = UIColor.white addSubview(contentView) imageView = UIImageView() - imageView.hidden = true + imageView.isHidden = true bgView = UIView() bgView.backgroundColor = ActorSDK.sharedActor().style.placeholderBgColor @@ -41,31 +41,31 @@ class AABigPlaceholderView: UIView { titleLabel = UILabel() titleLabel.textColor = ActorSDK.sharedActor().style.placeholderTitleColor - titleLabel.font = UIFont.systemFontOfSize(22) - titleLabel.textAlignment = NSTextAlignment.Center + titleLabel.font = UIFont.systemFont(ofSize: 22) + titleLabel.textAlignment = NSTextAlignment.center titleLabel.text = " " titleLabel.sizeToFit() contentView.addSubview(titleLabel) subtitleLabel = UILabel() subtitleLabel.textColor = ActorSDK.sharedActor().style.placeholderHintColor - subtitleLabel.font = UIFont.systemFontOfSize(16.0) - subtitleLabel.textAlignment = NSTextAlignment.Center + subtitleLabel.font = UIFont.systemFont(ofSize: 16.0) + subtitleLabel.textAlignment = NSTextAlignment.center subtitleLabel.numberOfLines = 0 contentView.addSubview(subtitleLabel) - actionButton = UIButton(type: .System) + actionButton = UIButton(type: .system) actionButton.titleLabel!.font = UIFont.mediumSystemFontOfSize(21) contentView.addSubview(actionButton) subtitle2Label = UILabel() subtitle2Label.textColor = ActorSDK.sharedActor().style.placeholderHintColor - subtitle2Label.font = UIFont.systemFontOfSize(16.0) - subtitle2Label.textAlignment = NSTextAlignment.Center + subtitle2Label.font = UIFont.systemFont(ofSize: 16.0) + subtitle2Label.textAlignment = NSTextAlignment.center subtitle2Label.numberOfLines = 0 contentView.addSubview(subtitle2Label) - action2Button = UIButton(type: .System) + action2Button = UIButton(type: .system) action2Button.titleLabel!.font = UIFont.mediumSystemFontOfSize(21) contentView.addSubview(action2Button) } @@ -78,73 +78,73 @@ class AABigPlaceholderView: UIView { // Setting image // - func setImage(image: UIImage?, title: String?, subtitle: String?) { + func setImage(_ image: UIImage?, title: String?, subtitle: String?) { setImage(image, title: title, subtitle: subtitle, actionTitle: nil, subtitle2: nil, actionTarget: nil, actionSelector: nil, action2title: nil, action2Selector: nil) } - func setImage(image: UIImage?, title: String?, subtitle: String?, actionTitle: String?, subtitle2: String?, actionTarget: AnyObject?, actionSelector: Selector?, action2title: String?, action2Selector: Selector?) { + func setImage(_ image: UIImage?, title: String?, subtitle: String?, actionTitle: String?, subtitle2: String?, actionTarget: AnyObject?, actionSelector: Selector?, action2title: String?, action2Selector: Selector?) { if image != nil { imageView.image = image! - imageView.hidden = false + imageView.isHidden = false } else { - imageView.hidden = true + imageView.isHidden = true } if title != nil { titleLabel.text = title - titleLabel.hidden = false + titleLabel.isHidden = false } else { - titleLabel.hidden = true + titleLabel.isHidden = true } if subtitle != nil { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineHeightMultiple = 1.11 - paragraphStyle.alignment = NSTextAlignment.Center + paragraphStyle.alignment = NSTextAlignment.center let attrString = NSMutableAttributedString(string: subtitle!) attrString.addAttribute(NSParagraphStyleAttributeName, value:paragraphStyle, range:NSMakeRange(0, attrString.length)) subtitleLabel.attributedText = attrString - subtitleLabel.hidden = false + subtitleLabel.isHidden = false } else { - subtitleLabel.hidden = true + subtitleLabel.isHidden = true } if actionTitle != nil && actionTarget != nil && actionSelector != nil { - actionButton.removeTarget(nil, action: nil, forControlEvents: UIControlEvents.AllEvents) - actionButton.addTarget(actionTarget!, action: actionSelector!, forControlEvents: UIControlEvents.TouchUpInside) - actionButton.setTitle(actionTitle, forState: UIControlState.Normal) - actionButton.hidden = false + actionButton.removeTarget(nil, action: nil, for: UIControlEvents.allEvents) + actionButton.addTarget(actionTarget!, action: actionSelector!, for: UIControlEvents.touchUpInside) + actionButton.setTitle(actionTitle, for: UIControlState()) + actionButton.isHidden = false } else { - actionButton.hidden = true + actionButton.isHidden = true } if (subtitle2 != nil) { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineHeightMultiple = 1.11 - paragraphStyle.alignment = NSTextAlignment.Center + paragraphStyle.alignment = NSTextAlignment.center let attrString = NSMutableAttributedString(string: subtitle2!) attrString.addAttribute(NSParagraphStyleAttributeName, value:paragraphStyle, range:NSMakeRange(0, attrString.length)) subtitle2Label.attributedText = attrString - subtitle2Label.hidden = false + subtitle2Label.isHidden = false } else { - subtitle2Label.hidden = true + subtitle2Label.isHidden = true } if action2title != nil && actionTarget != nil && actionSelector != nil { - action2Button.removeTarget(nil, action: nil, forControlEvents: UIControlEvents.AllEvents) - action2Button.addTarget(actionTarget!, action: action2Selector!, forControlEvents: UIControlEvents.TouchUpInside) - action2Button.setTitle(action2title, forState: UIControlState.Normal) - action2Button.hidden = false + action2Button.removeTarget(nil, action: nil, for: UIControlEvents.allEvents) + action2Button.addTarget(actionTarget!, action: action2Selector!, for: UIControlEvents.touchUpInside) + action2Button.setTitle(action2title, for: UIControlState()) + action2Button.isHidden = false } else { - action2Button.hidden = true + action2Button.isHidden = true } setNeedsLayout() @@ -160,7 +160,7 @@ class AABigPlaceholderView: UIView { var contentHeight: CGFloat = 0 let maxContentWidth = bounds.size.width - 40 - if imageView.hidden == false { + if imageView.isHidden == false { imageView.frame = CGRect(x: 20 + (maxContentWidth - imageView.image!.size.width) / 2.0, y: topOffset, width: imageView.image!.size.width, height: imageView.image!.size.height) contentHeight += imageView.image!.size.height + topOffset @@ -168,7 +168,7 @@ class AABigPlaceholderView: UIView { bgView.frame = CGRect(x: 0, y: 0, width: bounds.size.width, height: imageView.frame.height * 0.75 + topOffset) - if titleLabel.hidden == false { + if titleLabel.isHidden == false { if contentHeight > 0 { contentHeight += 10 } @@ -177,42 +177,42 @@ class AABigPlaceholderView: UIView { contentHeight += titleLabel.bounds.size.height } - if subtitleLabel.hidden == false { + if subtitleLabel.isHidden == false { if contentHeight > 0 { contentHeight += 14 } - let subtitleLabelSize = subtitleLabel.sizeThatFits(CGSize(width: maxContentWidth, height: CGFloat.max)) + let subtitleLabelSize = subtitleLabel.sizeThatFits(CGSize(width: maxContentWidth, height: CGFloat.greatestFiniteMagnitude)) subtitleLabel.frame = CGRect(x: 20, y: contentHeight, width: maxContentWidth, height: subtitleLabelSize.height) contentHeight += subtitleLabelSize.height } - if actionButton.hidden == false { + if actionButton.isHidden == false { if contentHeight > 0 { contentHeight += 14 } - let actionButtonTitleLabelSize = actionButton.titleLabel!.sizeThatFits(CGSize(width: maxContentWidth, height: CGFloat.max)) + let actionButtonTitleLabelSize = actionButton.titleLabel!.sizeThatFits(CGSize(width: maxContentWidth, height: CGFloat.greatestFiniteMagnitude)) actionButton.frame = CGRect(x: 20, y: contentHeight, width: maxContentWidth, height: actionButtonTitleLabelSize.height) contentHeight += actionButtonTitleLabelSize.height } - if subtitle2Label.hidden == false { + if subtitle2Label.isHidden == false { if contentHeight > 0 { contentHeight += 14 } - let subtitleLabelSize = subtitle2Label.sizeThatFits(CGSize(width: maxContentWidth, height: CGFloat.max)) + let subtitleLabelSize = subtitle2Label.sizeThatFits(CGSize(width: maxContentWidth, height: CGFloat.greatestFiniteMagnitude)) subtitle2Label.frame = CGRect(x: 20, y: contentHeight, width: maxContentWidth, height: subtitleLabelSize.height) contentHeight += subtitleLabelSize.height } - if action2Button.hidden == false { + if action2Button.isHidden == false { if contentHeight > 0 { contentHeight += 14 } - let actionButtonTitleLabelSize = action2Button.titleLabel!.sizeThatFits(CGSize(width: maxContentWidth, height: CGFloat.max)) + let actionButtonTitleLabelSize = action2Button.titleLabel!.sizeThatFits(CGSize(width: maxContentWidth, height: CGFloat.greatestFiniteMagnitude)) action2Button.frame = CGRect(x: 20, y: contentHeight, width: maxContentWidth, height: actionButtonTitleLabelSize.height) contentHeight += actionButtonTitleLabelSize.height } diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AACircleButton.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AACircleButton.swift index e0bd4c258e..7456a2a582 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AACircleButton.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AACircleButton.swift @@ -4,21 +4,21 @@ import Foundation -public class AACircleButton: UIView { +open class AACircleButton: UIView { - private let buttonSize: CGFloat + fileprivate let buttonSize: CGFloat - public let button = UIButton() - private let titleView = UILabel() - private let borderView = UIImageView() + open let button = UIButton() + fileprivate let titleView = UILabel() + fileprivate let borderView = UIImageView() - public var image: UIImage? { + open var image: UIImage? { didSet(v) { updateStyles() } } - public var title: String? { + open var title: String? { get { return titleView.text } @@ -27,21 +27,21 @@ public class AACircleButton: UIView { } } - public var filled: Bool = false { + open var filled: Bool = false { didSet(v) { updateStyles() } } - public var enabled: Bool = true { + open var enabled: Bool = true { didSet(v) { - button.enabled = v - button.userInteractionEnabled = v + button.isEnabled = v + button.isUserInteractionEnabled = v updateStyles() } } - public var borderColor: UIColor = UIColor.whiteColor().alpha(0.87) { + open var borderColor: UIColor = UIColor.white.alpha(0.87) { didSet(v) { updateStyles() } @@ -49,17 +49,17 @@ public class AACircleButton: UIView { public init(size: CGFloat) { self.buttonSize = size - super.init(frame: CGRectMake(0, 0, size, size)) + super.init(frame: CGRect(x: 0, y: 0, width: size, height: size)) - borderView.bounds = CGRectMake(0, 0, size, size) + borderView.bounds = CGRect(x: 0, y: 0, width: size, height: size) borderView.frame = borderView.bounds titleView.font = UIFont.thinSystemFontOfSize(17) - titleView.textAlignment = .Center - titleView.bounds = CGRectMake(0, 0, 86, 44) + titleView.textAlignment = .center + titleView.bounds = CGRect(x: 0, y: 0, width: 86, height: 44) titleView.adjustsFontSizeToFitWidth = true - button.bounds = CGRectMake(0, 0, size, size) + button.bounds = CGRect(x: 0, y: 0, width: size, height: size) updateStyles() @@ -72,24 +72,24 @@ public class AACircleButton: UIView { fatalError("init(coder:) has not been implemented") } - private func updateStyles() { - let mainColor = enabled ? UIColor.whiteColor() : UIColor.whiteColor().alpha(0.3) - let selectedColor = enabled ? UIColor.blackColor() : UIColor.blackColor().alpha(0.3) + fileprivate func updateStyles() { + let mainColor = enabled ? UIColor.white : UIColor.white.alpha(0.3) + let selectedColor = enabled ? UIColor.black : UIColor.black.alpha(0.3) titleView.textColor = mainColor if (filled) { borderView.image = Imaging.roundedImage(mainColor, radius: buttonSize / 2) - button.setImage(image?.tintImage(selectedColor), forState: .Normal) + button.setImage(image?.tintImage(selectedColor), for: UIControlState()) } else { borderView.image = Imaging.circleImage(mainColor, radius: buttonSize / 2) - button.setImage(image?.tintImage(mainColor), forState: .Normal) + button.setImage(image?.tintImage(mainColor), for: UIControlState()) } } - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() button.topIn(self.bounds) borderView.topIn(self.bounds) titleView.under(button.frame, offset: 5) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAMapFastView.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAMapFastView.swift index 8c7c792f5f..df67198b2e 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAMapFastView.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAMapFastView.swift @@ -5,9 +5,9 @@ import Foundation import MapKit -public class AAMapFastView: UIImageView { +open class AAMapFastView: UIImageView { - static private var mapCache = AASwiftlyLRU(capacity: 16) + static fileprivate var mapCache = AASwiftlyLRU(capacity: 16) let mapWidth: CGFloat let mapHeight: CGFloat @@ -23,7 +23,7 @@ public class AAMapFastView: UIImageView { fatalError("init(coder:) has not been implemented") } - func bind(latitude: Double, longitude: Double) { + func bind(_ latitude: Double, longitude: Double) { let key = "\(Int(latitude * 1000000))_\(Int(longitude * 1000000))" // Same Key @@ -41,11 +41,11 @@ public class AAMapFastView: UIImageView { let options = MKMapSnapshotOptions() options.region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)) - options.size = CGSizeMake(mapWidth, mapHeight) - options.scale = UIScreen.mainScreen().scale + options.size = CGSize(width: mapWidth, height: mapHeight) + options.scale = UIScreen.main.scale let snapshotter = MKMapSnapshotter(options: options) - snapshotter.startWithCompletionHandler { snapshot, error in + snapshotter.start (completionHandler: { snapshot, error in if let img = snapshot?.image { let rounded = img.roundCorners(img.size.width, h: img.size.height, roundSize: 14) dispatchOnUi { @@ -53,6 +53,6 @@ public class AAMapFastView: UIImageView { self.image = rounded } } - } + }) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAMapPinPointView.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAMapPinPointView.swift index b8aa1488c2..d38d10d673 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAMapPinPointView.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAMapPinPointView.swift @@ -4,25 +4,25 @@ import Foundation -public class AAMapPinPointView: UIView { +open class AAMapPinPointView: UIView { let pinView = UIImageView() let pinPointView = UIImageView() let pinShadowView = UIImageView() public init() { - super.init(frame: CGRectMake(0, 0, 100, 100)) + super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) - pinShadowView.frame = CGRectMake(43, 47, 32, 39) + pinShadowView.frame = CGRect(x: 43, y: 47, width: 32, height: 39) pinShadowView.alpha = 0.9 pinShadowView.image = UIImage.bundled("LocationPinShadow.png") addSubview(pinShadowView) - pinPointView.frame = CGRectMake(100 / 2 - 2, 100 - 18.5, 3.5, 1.5) + pinPointView.frame = CGRect(x: 100 / 2 - 2, y: 100 - 18.5, width: 3.5, height: 1.5) pinPointView.image = UIImage.bundled("LocationPinPoint.png") addSubview(pinPointView) - pinView.frame = CGRectMake(100 / 2 - 7, 47, 13.5, 36) + pinView.frame = CGRect(x: 100 / 2 - 7, y: 47, width: 13.5, height: 36) pinView.image = UIImage.bundled("LocationPin.png") addSubview(pinView) } @@ -31,45 +31,45 @@ public class AAMapPinPointView: UIView { fatalError("init(coder:) has not been implemented") } - func risePin(rised: Bool, animated: Bool) { + func risePin(_ rised: Bool, animated: Bool) { self.pinShadowView.layer.removeAllAnimations() self.pinView.layer.removeAllAnimations() if animated { if rised { - UIView.animateWithDuration(0.2, delay: 0.0, options: .BeginFromCurrentState, animations: { () -> Void in - self.pinView.frame = CGRectMake(100 / 2 - 7, 7, 13.5, 36) - self.pinShadowView.frame = CGRectMake(87, -33, 32, 39) + UIView.animate(withDuration: 0.2, delay: 0.0, options: .beginFromCurrentState, animations: { () -> Void in + self.pinView.frame = CGRect(x: 100 / 2 - 7, y: 7, width: 13.5, height: 36) + self.pinShadowView.frame = CGRect(x: 87, y: -33, width: 32, height: 39) }, completion: nil) } else { - UIView.animateWithDuration(0.2, delay: 0.0, options: .BeginFromCurrentState, animations: { () -> Void in - self.pinView.frame = CGRectMake(100 / 2 - 7, 47, 13.5, 36) - self.pinShadowView.frame = CGRectMake(43, 47, 32, 39) + UIView.animate(withDuration: 0.2, delay: 0.0, options: .beginFromCurrentState, animations: { () -> Void in + self.pinView.frame = CGRect(x: 100 / 2 - 7, y: 47, width: 13.5, height: 36) + self.pinShadowView.frame = CGRect(x: 43, y: 47, width: 32, height: 39) }, completion: { finished in if !finished { return } - UIView.animateWithDuration(0.1, delay: 0.0, options: .BeginFromCurrentState, animations: { () -> Void in - self.pinView.frame = CGRectMake(100 / 2 - 7, 47 + 2, 13.5, 36 - 2) + UIView.animate(withDuration: 0.1, delay: 0.0, options: .beginFromCurrentState, animations: { () -> Void in + self.pinView.frame = CGRect(x: 100 / 2 - 7, y: 47 + 2, width: 13.5, height: 36 - 2) }, completion: { (finished) -> Void in if !finished { return } - UIView.animateWithDuration(0.1, delay: 0.0, options: .BeginFromCurrentState, animations: { () -> Void in - self.pinView.frame = CGRectMake(100 / 2 - 7, 47, 13.5, 36) + UIView.animate(withDuration: 0.1, delay: 0.0, options: .beginFromCurrentState, animations: { () -> Void in + self.pinView.frame = CGRect(x: 100 / 2 - 7, y: 47, width: 13.5, height: 36) }, completion: nil) }) }) } } else { if rised { - self.pinView.frame = CGRectMake(100 / 2 - 7, 7, 13.5, 36) - self.pinShadowView.frame = CGRectMake(87, -33, 32, 39) + self.pinView.frame = CGRect(x: 100 / 2 - 7, y: 7, width: 13.5, height: 36) + self.pinShadowView.frame = CGRect(x: 87, y: -33, width: 32, height: 39) } else { - self.pinView.frame = CGRectMake(100 / 2 - 7, 47, 13.5, 36) - self.pinShadowView.frame = CGRectMake(43, 47, 32, 39) + self.pinView.frame = CGRect(x: 100 / 2 - 7, y: 47, width: 13.5, height: 36) + self.pinShadowView.frame = CGRect(x: 43, y: 47, width: 32, height: 39) } } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAProgressView.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAProgressView.swift index 07778337ef..0c9f93a32a 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAProgressView.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAProgressView.swift @@ -5,32 +5,32 @@ import Foundation import VBFPopFlatButton -public class AAProgressView: UIView { +open class AAProgressView: UIView { - private let circlePathLayer = CAShapeLayer() - private let backgroundPathLayer = CAShapeLayer() - private var progressButton: VBFPopFlatButton! + fileprivate let circlePathLayer = CAShapeLayer() + fileprivate let backgroundPathLayer = CAShapeLayer() + fileprivate var progressButton: VBFPopFlatButton! public init(size: CGSize) { - super.init(frame: CGRectMake(0, 0, size.width, size.height)) + super.init(frame: CGRect(x: 0, y: 0, width: size.width, height: size.height)) - self.backgroundColor = UIColor.clearColor() - self.userInteractionEnabled = false + self.backgroundColor = UIColor.clear + self.isUserInteractionEnabled = false - let bgPath = UIBezierPath(ovalInRect: CGRectMake(0, 0, self.bounds.width, self.bounds.height)) + let bgPath = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: self.bounds.width, height: self.bounds.height)) backgroundPathLayer.frame = bounds - backgroundPathLayer.path = bgPath.CGPath - backgroundPathLayer.fillColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0x64/255.0).CGColor - backgroundPathLayer.strokeColor = UIColor.clearColor().CGColor + backgroundPathLayer.path = bgPath.cgPath + backgroundPathLayer.fillColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0x64/255.0).cgColor + backgroundPathLayer.strokeColor = UIColor.clear.cgColor layer.addSublayer(backgroundPathLayer) - let circlePath = UIBezierPath(ovalInRect: CGRectMake(3, 3, self.bounds.width - 6, self.bounds.height - 6)) + let circlePath = UIBezierPath(ovalIn: CGRect(x: 3, y: 3, width: self.bounds.width - 6, height: self.bounds.height - 6)) circlePathLayer.frame = bounds - circlePathLayer.path = circlePath.CGPath + circlePathLayer.path = circlePath.cgPath circlePathLayer.lineWidth = 3 circlePathLayer.lineCap = kCALineCapRound - circlePathLayer.fillColor = UIColor.clearColor().CGColor - circlePathLayer.strokeColor = UIColor.whiteColor().CGColor + circlePathLayer.fillColor = UIColor.clear.cgColor + circlePathLayer.strokeColor = UIColor.white.cgColor circlePathLayer.strokeStart = 0 circlePathLayer.strokeEnd = 0.5 layer.addSublayer(circlePathLayer) @@ -56,33 +56,33 @@ public class AAProgressView: UIView { fatalError("init(coder:) has not been implemented") } - public func setButtonType(type: FlatButtonType, animated: Bool) { + open func setButtonType(_ type: FlatButtonType, animated: Bool) { if progressButton != nil && animated { - progressButton.animateToType(type) + progressButton.animate(to: type) } else { hideButton() let size: CGFloat = self.bounds.width < 64 ? 24 : 32 let x = (self.bounds.width - size) / 2 let y = (self.bounds.height - size) / 2 progressButton = VBFPopFlatButton(frame: CGRect(x: x, y: y, width: size, height: size), buttonType: type, buttonStyle: FlatButtonStyle.buttonPlainStyle, animateToInitialState: animated) - progressButton.userInteractionEnabled = false + progressButton.isUserInteractionEnabled = false self.addSubview(progressButton) } } - public func setProgress(value: Double) { + open func setProgress(_ value: Double) { circlePathLayer.strokeEnd = CGFloat(value) } - public func hideProgress() { + open func hideProgress() { circlePathLayer.strokeEnd = 0 } - public func hideButton() { + open func hideButton() { if progressButton != nil { progressButton.removeFromSuperview() progressButton = nil } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAStickerView.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAStickerView.swift index 63e1bd8a8c..030c2919ec 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAStickerView.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AAStickerView.swift @@ -5,16 +5,16 @@ import Foundation import YYImage -public class AAStickerView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { +open class AAStickerView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { - private var file: ACFileReference? + fileprivate var file: ACFileReference? public init() { - super.init(frame: CGRectZero) + super.init(frame: CGRect.zero) layer.delegate = self - layer.contentsScale = UIScreen.mainScreen().scale - backgroundColor = UIColor.clearColor() - Actor.subscribeToDownloads(self) + layer.contentsScale = UIScreen.main.scale + backgroundColor = UIColor.clear + Actor.subscribe(toDownloads: self) } public required init?(coder aDecoder: NSCoder) { @@ -22,10 +22,10 @@ public class AAStickerView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { } deinit { - Actor.unsubscribeFromDownloads(self) + Actor.unsubscribe(fromDownloads: self) } - public func onDownloadedWithLong(fileId: jlong) { + open func onDownloaded(withLong fileId: jlong) { if self.file?.getFileId() == fileId { dispatchOnUi { if self.file?.getFileId() == fileId { @@ -35,16 +35,16 @@ public class AAStickerView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { } } - public func setSticker(file: ACFileReference?) { + open func setSticker(_ file: ACFileReference?) { self.file = file self.layer.setNeedsDisplay() } - public override class func layerClass() -> AnyClass { + open override class var layerClass : AnyClass { return YYAsyncLayer.self } - public func newAsyncDisplayTask() -> YYAsyncLayerDisplayTask { + open func newAsyncDisplayTask() -> YYAsyncLayerDisplayTask { let res = YYAsyncLayerDisplayTask() @@ -52,7 +52,7 @@ public class AAStickerView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { res.display = { (context: CGContext, size: CGSize, isCancelled: () -> Bool) -> () in if _file != nil { - let desc = Actor.findDownloadedDescriptorWithFileId(_file!.getFileId()) + let desc = Actor.findDownloadedDescriptor(withFileId: _file!.getFileId()) if isCancelled() { return } @@ -62,14 +62,14 @@ public class AAStickerView: UIView, YYAsyncLayerDelegate, ACFileEventCallback { if isCancelled() { return } - image?.drawInRect(CGRectMake(0, 0, size.width, size.height), withContentMode: UIViewContentMode.ScaleAspectFit, clipsToBounds: true) + image?.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height), with: UIViewContentMode.scaleAspectFit, clipsToBounds: true) } else { // Request if not available - Actor.startDownloadingWithReference(_file!) + Actor.startDownloading(with: _file!) } } } return res } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AATableViewHeader.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AATableViewHeader.swift index 70481487c3..62ef048b65 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AATableViewHeader.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AATableViewHeader.swift @@ -4,21 +4,21 @@ import Foundation -public class AATableViewHeader: UIView { +open class AATableViewHeader: UIView { - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() for view in self.subviews { - view.frame = CGRectMake(view.frame.minX, view.frame.minY, self.frame.width, view.frame.height) + view.frame = CGRect(x: view.frame.minX, y: view.frame.minY, width: self.frame.width, height: view.frame.height) // Fix for UISearchBar disappear // http://stackoverflow.com/questions/19044156/searchbar-disappears-from-headerview-in-ios-7 if let search = view as? UISearchBar { if let buggyView = search.subviews.first { buggyView.bounds = search.bounds - buggyView.center = CGPointMake(buggyView.bounds.width/2,buggyView.bounds.height/2) + buggyView.center = CGPoint(x: buggyView.bounds.width/2,y: buggyView.bounds.height/2) } } } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AATableViewSeparator.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AATableViewSeparator.swift index fc450b640e..bf0eb532ea 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AATableViewSeparator.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/AATableViewSeparator.swift @@ -8,7 +8,7 @@ import UIKit class AATableViewSeparator : UIView { init(color: UIColor) { - super.init(frame: CGRectZero) + super.init(frame: CGRect.zero) super.backgroundColor = color } @@ -25,4 +25,4 @@ import UIKit } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AAAvatarCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AAAvatarCell.swift index 002b9e7f5e..6e4d2be5e7 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AAAvatarCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AAAvatarCell.swift @@ -4,35 +4,35 @@ import Foundation -public class AAAvatarCell: AATableViewCell { +open class AAAvatarCell: AATableViewCell { - public var titleLabel = UILabel() - public var subtitleLabel = UILabel() - public var avatarView = AAAvatarView() - public var progress = UIActivityIndicatorView(activityIndicatorStyle: .White) - public var didTap: ((view: UIView)->())? + open var titleLabel = UILabel() + open var subtitleLabel = UILabel() + open var avatarView = AAAvatarView() + open var progress = UIActivityIndicatorView(activityIndicatorStyle: .white) + open var didTap: ((_ view: UIView)->())? public override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) avatarView = AAAvatarView() avatarView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(AAAvatarCell.avatarDidTap))) - avatarView.userInteractionEnabled = true + avatarView.isUserInteractionEnabled = true contentView.addSubview(avatarView) - titleLabel.backgroundColor = UIColor.clearColor() + titleLabel.backgroundColor = UIColor.clear titleLabel.textColor = ActorSDK.sharedActor().style.cellTextColor - titleLabel.font = UIFont.systemFontOfSize(20.0) + titleLabel.font = UIFont.systemFont(ofSize: 20.0) contentView.addSubview(titleLabel) - subtitleLabel.backgroundColor = UIColor.clearColor() + subtitleLabel.backgroundColor = UIColor.clear subtitleLabel.textColor = ActorSDK.sharedActor().style.cellHintColor - subtitleLabel.font = UIFont.systemFontOfSize(14.0) + subtitleLabel.font = UIFont.systemFont(ofSize: 14.0) contentView.addSubview(subtitleLabel) contentView.addSubview(progress) - selectionStyle = .None + selectionStyle = .none } public required init(coder aDecoder: NSCoder) { @@ -40,20 +40,20 @@ public class AAAvatarCell: AATableViewCell { } func avatarDidTap() { - didTap?(view: avatarView) + didTap?(avatarView) } - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() avatarView.frame = CGRect(x: 14, y: 14, width: 66, height: 66) progress.frame = avatarView.frame - if subtitleLabel.hidden { + if subtitleLabel.isHidden { titleLabel.frame = CGRect(x: 82 + 6, y: 14 + 64/2 - 14, width: self.contentView.bounds.width - 82 - 14 - 10, height: 24) } else { titleLabel.frame = CGRect(x: 82 + 6, y: 14 + 64/2 - 24, width: self.contentView.bounds.width - 82 - 14 - 10, height: 24) subtitleLabel.frame = CGRect(x: 82 + 6, y: 14 + 66/2 + 4, width: self.contentView.bounds.width - 82 - 14 - 10, height: 16) } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AABackgroundCellRenderer.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AABackgroundCellRenderer.swift index 5f0f36338d..2349f4e431 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AABackgroundCellRenderer.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AABackgroundCellRenderer.swift @@ -5,20 +5,20 @@ import Foundation -public class AABackgroundCellRenderer { +open class AABackgroundCellRenderer where T: AnyObject, P: AnyObject, P: Equatable { - private var generation = 0 - private var wasPresented: Bool = false - private var requestedConfig: P? = nil - private let renderer: (config: P)-> T! - private let receiver: (T!) -> () + fileprivate var generation = 0 + fileprivate var wasPresented: Bool = false + fileprivate var requestedConfig: P? = nil + fileprivate let renderer: (_ config: P)-> T! + fileprivate let receiver: (T!) -> () - public init(renderer: (config: P) -> T!, receiver: (T!) -> ()) { + public init(renderer: @escaping (_ config: P) -> T!, receiver: @escaping (T!) -> ()) { self.renderer = renderer self.receiver = receiver } - func requestRender(config: P) -> Bool { + func requestRender(_ config: P) -> Bool { // Ignore if not resized if requestedConfig == config { return false @@ -44,7 +44,7 @@ public class AABackgroundCellRenderer ())? - public var contentInset: CGFloat = 15 + open var style: AACommonCellStyle = .normal { didSet { updateCellStyle() } } + open var switchBlock: ((Bool) -> ())? + open var contentInset: CGFloat = 15 public override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - titleLabel.font = UIFont.systemFontOfSize(17.0) + titleLabel.font = UIFont.systemFont(ofSize: 17.0) contentView.addSubview(titleLabel) - hintLabel.font = UIFont.systemFontOfSize(17.0) + hintLabel.font = UIFont.systemFont(ofSize: 17.0) hintLabel.textColor = appStyle.cellHintColor contentView.addSubview(hintLabel) } @@ -43,99 +43,100 @@ public class AACommonCell: AATableViewCell { // Setting text content - public func setContent(content: String?) { + open func setContent(_ content: String?) { titleLabel.text = content } - public func setHint(hint: String?) { + open func setHint(_ hint: String?) { if hint == nil { - hintLabel.hidden = true + hintLabel.isHidden = true } else { hintLabel.text = hint - hintLabel.hidden = false + hintLabel.isHidden = false } + setNeedsLayout() } // Setting switcher content - public func setSwitcherOn(on: Bool) { + open func setSwitcherOn(_ on: Bool) { setSwitcherOn(on, animated: false) } - public func setSwitcherOn(on: Bool, animated: Bool) { + open func setSwitcherOn(_ on: Bool, animated: Bool) { switcher?.setOn(on, animated: animated) } - public func setSwitcherEnabled(enabled: Bool) { - switcher?.enabled = enabled + open func setSwitcherEnabled(_ enabled: Bool) { + switcher?.isEnabled = enabled } // Private methods - private func updateCellStyle() { + fileprivate func updateCellStyle() { switch (style) { - case .Normal: + case .normal: titleLabel.textColor = appStyle.cellTextColor - titleLabel.textAlignment = NSTextAlignment.Left - switcher?.hidden = true - accessoryType = UITableViewCellAccessoryType.None + titleLabel.textAlignment = NSTextAlignment.left + switcher?.isHidden = true + accessoryType = UITableViewCellAccessoryType.none break - case .Hint: + case .hint: titleLabel.textColor = appStyle.cellHintColor - titleLabel.textAlignment = NSTextAlignment.Left - switcher?.hidden = true - accessoryType = UITableViewCellAccessoryType.None + titleLabel.textAlignment = NSTextAlignment.left + switcher?.isHidden = true + accessoryType = UITableViewCellAccessoryType.none break - case .DestructiveCentered: + case .destructiveCentered: titleLabel.textColor = appStyle.cellDestructiveColor - titleLabel.textAlignment = NSTextAlignment.Center - switcher?.hidden = true - accessoryType = UITableViewCellAccessoryType.None + titleLabel.textAlignment = NSTextAlignment.center + switcher?.isHidden = true + accessoryType = UITableViewCellAccessoryType.none break - case .Destructive: + case .destructive: titleLabel.textColor = appStyle.cellDestructiveColor - titleLabel.textAlignment = NSTextAlignment.Left - switcher?.hidden = true - accessoryType = UITableViewCellAccessoryType.None + titleLabel.textAlignment = NSTextAlignment.left + switcher?.isHidden = true + accessoryType = UITableViewCellAccessoryType.none break - case .Switch: + case .switch: titleLabel.textColor = appStyle.cellTextColor - titleLabel.textAlignment = NSTextAlignment.Left + titleLabel.textAlignment = NSTextAlignment.left setupSwitchIfNeeded() - switcher?.hidden = false - accessoryType = UITableViewCellAccessoryType.None + switcher?.isHidden = false + accessoryType = UITableViewCellAccessoryType.none break - case .Action: + case .action: titleLabel.textColor = appStyle.cellTintColor - titleLabel.textAlignment = NSTextAlignment.Left - switcher?.hidden = true - accessoryType = UITableViewCellAccessoryType.None + titleLabel.textAlignment = NSTextAlignment.left + switcher?.isHidden = true + accessoryType = UITableViewCellAccessoryType.none break - case .ActionCentered: + case .actionCentered: titleLabel.textColor = appStyle.cellTintColor - titleLabel.textAlignment = NSTextAlignment.Center - switcher?.hidden = true - accessoryType = UITableViewCellAccessoryType.None + titleLabel.textAlignment = NSTextAlignment.center + switcher?.isHidden = true + accessoryType = UITableViewCellAccessoryType.none break - case .Navigation: + case .navigation: titleLabel.textColor = appStyle.cellTextColor - titleLabel.textAlignment = NSTextAlignment.Left - switcher?.hidden = true - accessoryType = UITableViewCellAccessoryType.DisclosureIndicator + titleLabel.textAlignment = NSTextAlignment.left + switcher?.isHidden = true + accessoryType = UITableViewCellAccessoryType.disclosureIndicator - case .Checkmark: + case .checkmark: titleLabel.textColor = appStyle.cellTextColor - titleLabel.textAlignment = NSTextAlignment.Left - switcher?.hidden = true - accessoryType = UITableViewCellAccessoryType.Checkmark + titleLabel.textAlignment = NSTextAlignment.left + switcher?.isHidden = true + accessoryType = UITableViewCellAccessoryType.checkmark break } } - private func setupSwitchIfNeeded() { + fileprivate func setupSwitchIfNeeded() { if switcher == nil { switcher = UISwitch() - switcher!.addTarget(self, action: #selector(AACommonCell.switcherSwitched), forControlEvents: UIControlEvents.ValueChanged) + switcher!.addTarget(self, action: #selector(AACommonCell.switcherSwitched), for: UIControlEvents.valueChanged) switcher!.onTintColor = appStyle.vcSwitchOn switcher!.tintColor = appStyle.vcSwitchOff contentView.addSubview(switcher!) @@ -144,21 +145,26 @@ public class AACommonCell: AATableViewCell { func switcherSwitched() { if switchBlock != nil { - switchBlock!(switcher!.on) + switchBlock!(switcher!.isOn) } } - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() if hintLabel.text != nil { - hintLabel.frame = CGRectMake(0, 0, 100, 44) + hintLabel.frame = CGRect(x: 0, y: 0, width: 100, height: 44) hintLabel.sizeToFit() - hintLabel.frame = CGRectMake(contentView.bounds.width - hintLabel.width, 0, hintLabel.width, 44) - titleLabel.frame = CGRectMake(contentInset, 0, contentView.bounds.width - hintLabel.width - contentInset - 5, 44) + if accessoryType == UITableViewCellAccessoryType.none { + hintLabel.frame = CGRect(x: contentView.bounds.width - hintLabel.width - 15, y: 0, width: hintLabel.width, height: 44) + titleLabel.frame = CGRect(x: contentInset, y: 0, width: contentView.bounds.width - hintLabel.width - contentInset - 20, height: 44) + } else { + hintLabel.frame = CGRect(x: contentView.bounds.width - hintLabel.width, y: 0, width: hintLabel.width, height: 44) + titleLabel.frame = CGRect(x: contentInset, y: 0, width: contentView.bounds.width - hintLabel.width - contentInset - 5, height: 44) + } } else { - titleLabel.frame = CGRectMake(contentInset, 0, contentView.bounds.width - contentInset - 5, 44) + titleLabel.frame = CGRect(x: contentInset, y: 0, width: contentView.bounds.width - contentInset - 5, height: 44) } if switcher != nil { @@ -166,4 +172,4 @@ public class AACommonCell: AATableViewCell { switcher!.frame = CGRect(x: contentView.bounds.width - switcherSize.width - 15, y: (contentView.bounds.height - switcherSize.height) / 2, width: switcherSize.width, height: switcherSize.height) } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AAEditCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AAEditCell.swift index c57c0eac79..158d002f76 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AAEditCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AAEditCell.swift @@ -4,18 +4,22 @@ import Foundation -public class AAEditCell: AATableViewCell { +open class AAEditCell: AATableViewCell { - public let textField = UITextField() + open let textPrefix = UILabel() + open let textField = UITextField() public override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - textField.autocapitalizationType = .None - textField.autocorrectionType = .No + textField.autocapitalizationType = .none + textField.autocorrectionType = .no textField.textColor = appStyle.cellTextColor - textField.keyboardAppearance = appStyle.isDarkApp ? .Dark : .Light + textField.keyboardAppearance = appStyle.isDarkApp ? .dark : .light + textPrefix.isHidden = true + + contentView.addSubview(textPrefix) contentView.addSubview(textField) } @@ -23,9 +27,16 @@ public class AAEditCell: AATableViewCell { fatalError("init(coder:) has not been implemented") } - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() - textField.frame = CGRectMake(15, 0, contentView.width - 30, 44) + if textPrefix.isHidden { + textField.frame = CGRect(x: 15, y: 0, width: contentView.width - 30, height: 44) + } else { + textPrefix.frame = CGRect(x: 15, y: 0, width: contentView.width - 30, height: 44) + textPrefix.sizeToFit() + textPrefix.frame = CGRect(x: 15, y: 0, width: textPrefix.width, height: 44) + textField.frame = CGRect(x: 15 + textPrefix.width, y: 0, width: contentView.width - textPrefix.width - 30, height: 44) + } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AAHeaderCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AAHeaderCell.swift index 382a8cf552..f2420cc958 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AAHeaderCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AAHeaderCell.swift @@ -4,22 +4,22 @@ import Foundation -public class AAHeaderCell: AATableViewCell { +open class AAHeaderCell: AATableViewCell { - public var titleView = UILabel() - public var iconView = UIImageView() + open var titleView = UILabel() + open var iconView = UIImageView() public override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) contentView.backgroundColor = appStyle.vcBackyardColor - selectionStyle = UITableViewCellSelectionStyle.None + selectionStyle = UITableViewCellSelectionStyle.none titleView.textColor = appStyle.cellHeaderColor - titleView.font = UIFont.systemFontOfSize(14) + titleView.font = UIFont.systemFont(ofSize: 14) contentView.addSubview(titleView) - iconView.contentMode = UIViewContentMode.ScaleAspectFill + iconView.contentMode = UIViewContentMode.scaleAspectFill contentView.addSubview(iconView) } @@ -27,13 +27,13 @@ public class AAHeaderCell: AATableViewCell { fatalError("init(coder:) has not been implemented") } - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() let height = self.contentView.bounds.height let width = self.contentView.bounds.width - titleView.frame = CGRectMake(15, height - 28, width - 48, 24) - iconView.frame = CGRectMake(width - 18 - 15, height - 18 - 4, 18, 18) + titleView.frame = CGRect(x: 15, y: height - 28, width: width - 48, height: 24) + iconView.frame = CGRect(x: width - 18 - 15, y: height - 18 - 4, width: 18, height: 18) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AATableViewCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AATableViewCell.swift index ed9ae747a3..8bbd68bbde 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AATableViewCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AATableViewCell.swift @@ -4,12 +4,12 @@ import Foundation -public class AATableViewCell: UITableViewCell { +open class AATableViewCell: UITableViewCell { - private static let separatorColor = ActorSDK.sharedActor().style.vcSeparatorColor + fileprivate static let separatorColor = ActorSDK.sharedActor().style.vcSeparatorColor - private var topSeparator: UIView = UIView() - private var bottomSeparator: UIView = UIView() + fileprivate var topSeparator: UIView = UIView() + fileprivate var bottomSeparator: UIView = UIView() var appStyle: ActorStyle { get { @@ -35,18 +35,18 @@ public class AATableViewCell: UITableViewCell { } - public var topSeparatorLeftInset: CGFloat = 0.0 { + open var topSeparatorLeftInset: CGFloat = 0.0 { didSet { setNeedsLayout() } } - public var bottomSeparatorLeftInset: CGFloat = 0.0 { + open var bottomSeparatorLeftInset: CGFloat = 0.0 { didSet { setNeedsLayout() } } - public var topSeparatorVisible: Bool = false { + open var topSeparatorVisible: Bool = false { didSet { if topSeparatorVisible == oldValue { return @@ -54,7 +54,7 @@ public class AATableViewCell: UITableViewCell { if topSeparatorVisible { contentView.addSubview(topSeparator) - contentView.bringSubviewToFront(topSeparator) + contentView.bringSubview(toFront: topSeparator) } else { topSeparator.removeFromSuperview() } @@ -63,7 +63,7 @@ public class AATableViewCell: UITableViewCell { } } - public var bottomSeparatorVisible: Bool = false { + open var bottomSeparatorVisible: Bool = false { didSet { if bottomSeparatorVisible == oldValue { return @@ -80,22 +80,22 @@ public class AATableViewCell: UITableViewCell { } } - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() if topSeparatorVisible { topSeparator.frame = CGRect(x: topSeparatorLeftInset, y: 0, width: bounds.width - topSeparatorLeftInset, height: 0.5) - contentView.bringSubviewToFront(topSeparator) + contentView.bringSubview(toFront: topSeparator) } if bottomSeparatorVisible { bottomSeparator.frame = CGRect(x: bottomSeparatorLeftInset, y: contentView.bounds.height - 0.5, width: bounds.width - bottomSeparatorLeftInset, height: 0.5) - contentView.bringSubviewToFront(bottomSeparator) + contentView.bringSubview(toFront: bottomSeparator) } } - public override func setHighlighted(highlighted: Bool, animated: Bool) { - if self.highlighted != highlighted { + open override func setHighlighted(_ highlighted: Bool, animated: Bool) { + if self.isHighlighted != highlighted { super.setHighlighted(highlighted, animated: animated) } @@ -109,8 +109,8 @@ public class AATableViewCell: UITableViewCell { } } - public override func setSelected(selected: Bool, animated: Bool) { - if self.selected != selected { + open override func setSelected(_ selected: Bool, animated: Bool) { + if self.isSelected != selected { super.setSelected(selected, animated: animated) } @@ -123,4 +123,4 @@ public class AATableViewCell: UITableViewCell { } } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AATextCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AATextCell.swift index 57cac77545..0af8a3c42f 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AATextCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AATextCell.swift @@ -4,24 +4,24 @@ import Foundation -public class AATextCell: AATableViewCell { +open class AATextCell: AATableViewCell { - public var titleLabel: UILabel = UILabel() - public var contentLabel: UILabel = UILabel() + open var titleLabel: UILabel = UILabel() + open var contentLabel: UILabel = UILabel() public override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - titleLabel.font = UIFont.systemFontOfSize(14.0) + titleLabel.font = UIFont.systemFont(ofSize: 14.0) titleLabel.text = " " titleLabel.sizeToFit() titleLabel.textColor = appStyle.cellTintColor contentView.addSubview(titleLabel) - contentLabel.font = UIFont.systemFontOfSize(17.0) + contentLabel.font = UIFont.systemFont(ofSize: 17.0) contentLabel.text = " " contentLabel.textColor = appStyle.cellTextColor - contentLabel.lineBreakMode = NSLineBreakMode.ByWordWrapping + contentLabel.lineBreakMode = NSLineBreakMode.byWordWrapping contentLabel.numberOfLines = 0 contentLabel.sizeToFit() contentView.addSubview(contentLabel) @@ -31,9 +31,11 @@ public class AATextCell: AATableViewCell { fatalError("init(coder:) has not been implemented") } - public func setContent(title: String?, content: String?, isAction: Bool) { + open func setContent(_ title: String?, content: String?, isAction: Bool) { titleLabel.text = title + titleLabel.isHidden = title == nil contentLabel.text = content + if isAction { contentLabel.textColor = appStyle.cellTintColor } else { @@ -41,16 +43,25 @@ public class AATextCell: AATableViewCell { } } - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() - titleLabel.frame = CGRect(x: 15, y: 7, width: contentView.bounds.width - 30, height: titleLabel.bounds.height) - contentLabel.frame = CGRect(x: 15, y: 27, width: contentView.bounds.width - 30, height: 10000) - contentLabel.sizeToFit() + if titleLabel.isHidden { + contentLabel.frame = CGRect(x: 15, y: 8, width: contentView.bounds.width - 30, height: contentView.height - 16) + } else { + titleLabel.frame = CGRect(x: 15, y: 7, width: contentView.bounds.width - 30, height: titleLabel.bounds.height) + contentLabel.frame = CGRect(x: 15, y: 27, width: contentView.bounds.width - 30, height: 10000) + contentLabel.sizeToFit() + } } - public class func measure(text: String, width: CGFloat, enableNavigation: Bool) -> CGFloat { + open class func measure(_ title: String?, text: String, width: CGFloat, enableNavigation: Bool) -> CGFloat { let size = UIViewMeasure.measureText(text, width: width - 30 - (enableNavigation ? 30 : 0), fontSize: 17) - return CGFloat(size.height + 36) + + if title != nil { + return CGFloat(size.height + 36) + } else { + return CGFloat(max(size.height + 16, 44)) + } } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AATitledCell.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AATitledCell.swift index 4c4838101c..e9f4212da0 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AATitledCell.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/Cells/AATitledCell.swift @@ -4,11 +4,11 @@ import UIKit -public class AATitledCell: AATableViewCell { +open class AATitledCell: AATableViewCell { - private var isAction: Bool = false - public let titleLabel: UILabel = UILabel() - public let contentLabel: UILabel = UILabel() + fileprivate var isAction: Bool = false + open let titleLabel: UILabel = UILabel() + open let contentLabel: UILabel = UILabel() public override init(style: UITableViewCellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -22,17 +22,17 @@ public class AATitledCell: AATableViewCell { fatalError("init(coder:) has not been implemented") } - public func setContent(title: String, content: String, isAction: Bool) { + open func setContent(_ title: String, content: String, isAction: Bool) { titleLabel.text = title contentLabel.text = content if isAction { - contentLabel.textColor = UIColor.lightGrayColor() + contentLabel.textColor = UIColor.lightGray } else { contentLabel.textColor = appStyle.cellTextColor } } - public override func layoutSubviews() { + open override func layoutSubviews() { super.layoutSubviews() titleLabel.frame = CGRect(x: separatorInset.left, y: 7, width: contentView.bounds.width - separatorInset.left - 10, height: 19) diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/ViewExtensions.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/ViewExtensions.swift index 2c9028e556..84a0c79463 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/ViewExtensions.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/Views/ViewExtensions.swift @@ -15,7 +15,7 @@ public extension UITabBarItem { self.init(title: AALocalized(title), image: UIImage.tinted(img, color: unselectedIcon), selectedImage: UIImage.tinted(selImage, color: selectedIcon)) - setTitleTextAttributes([NSForegroundColorAttributeName: unselectedText], forState: UIControlState.Normal) - setTitleTextAttributes([NSForegroundColorAttributeName: selectedText], forState: UIControlState.Selected) + setTitleTextAttributes([NSForegroundColorAttributeName: unselectedText], for: UIControlState()) + setTitleTextAttributes([NSForegroundColorAttributeName: selectedText], for: UIControlState.selected) } -} \ No newline at end of file +} diff --git a/actor-sdk/sdk-core-ios/ActorSDK/Sources/WebRTCExt.swift b/actor-sdk/sdk-core-ios/ActorSDK/Sources/WebRTCExt.swift index 536238f1c3..b70c9c19c4 100644 --- a/actor-sdk/sdk-core-ios/ActorSDK/Sources/WebRTCExt.swift +++ b/actor-sdk/sdk-core-ios/ActorSDK/Sources/WebRTCExt.swift @@ -9,54 +9,54 @@ class AAPeerConnectionDelegate: NSObject, RTCPeerConnectionDelegate { var onCandidateReceived: ((RTCICECandidate)->())? var onStreamAdded: ((RTCMediaStream) -> ())? - func peerConnection(peerConnection: RTCPeerConnection!, signalingStateChanged stateChanged: RTCSignalingState) { + func peerConnection(_ peerConnection: RTCPeerConnection!, signalingStateChanged stateChanged: RTCSignalingState) { } - func peerConnection(peerConnection: RTCPeerConnection!, addedStream stream: RTCMediaStream!) { + func peerConnection(_ peerConnection: RTCPeerConnection!, addedStream stream: RTCMediaStream!) { onStreamAdded?(stream) } - func peerConnection(peerConnection: RTCPeerConnection!, removedStream stream: RTCMediaStream!) { + func peerConnection(_ peerConnection: RTCPeerConnection!, removedStream stream: RTCMediaStream!) { } - func peerConnectionOnRenegotiationNeeded(peerConnection: RTCPeerConnection!) { + func peerConnection(onRenegotiationNeeded peerConnection: RTCPeerConnection!) { } - func peerConnection(peerConnection: RTCPeerConnection!, iceConnectionChanged newState: RTCICEConnectionState) { + func peerConnection(_ peerConnection: RTCPeerConnection!, iceConnectionChanged newState: RTCICEConnectionState) { } - func peerConnection(peerConnection: RTCPeerConnection!, iceGatheringChanged newState: RTCICEGatheringState) { + func peerConnection(_ peerConnection: RTCPeerConnection!, iceGatheringChanged newState: RTCICEGatheringState) { } - func peerConnection(peerConnection: RTCPeerConnection!, gotICECandidate candidate: RTCICECandidate!) { + func peerConnection(_ peerConnection: RTCPeerConnection!, gotICECandidate candidate: RTCICECandidate!) { onCandidateReceived?(candidate) } - func peerConnection(peerConnection: RTCPeerConnection!, didOpenDataChannel dataChannel: RTCDataChannel!) { + func peerConnection(_ peerConnection: RTCPeerConnection!, didOpen dataChannel: RTCDataChannel!) { } } class AASessionDescriptionCreateDelegate: NSObject, RTCSessionDescriptionDelegate { - let didCreate: (RTCSessionDescription!, NSError!) -> () + let didCreate: (RTCSessionDescription?, Error?) -> () let peerConnection: RTCPeerConnection - init(didCreate: (RTCSessionDescription!, NSError!) -> (), peerConnection: RTCPeerConnection) { + init(didCreate: @escaping (RTCSessionDescription?, Error?) -> (), peerConnection: RTCPeerConnection) { self.didCreate = didCreate self.peerConnection = peerConnection } - func peerConnection(peerConnection: RTCPeerConnection!, didCreateSessionDescription sdp: RTCSessionDescription!, error: NSError!) { + func peerConnection(_ peerConnection: RTCPeerConnection!, didCreateSessionDescription sdp: RTCSessionDescription!, error: Error!) { didCreate(sdp!, error) } - func peerConnection(peerConnection: RTCPeerConnection!, didSetSessionDescriptionWithError error: NSError!) { + func peerConnection(_ peerConnection: RTCPeerConnection!, didSetSessionDescriptionWithError error: Error!) { } } @@ -65,10 +65,10 @@ class AASessionDescriptionCreateDelegate: NSObject, RTCSessionDescriptionDelegat private var sessionSetTarget = "descTarget" class AASessionDescriptionSetDelegate: NSObject, RTCSessionDescriptionDelegate { - let didSet: (NSError!) -> () + let didSet: (Error!) -> () let peerConnection: RTCPeerConnection - init(didSet: (NSError!) -> (), peerConnection: RTCPeerConnection) { + init(didSet: @escaping (Error!) -> (), peerConnection: RTCPeerConnection) { self.didSet = didSet self.peerConnection = peerConnection super.init() @@ -76,11 +76,11 @@ class AASessionDescriptionSetDelegate: NSObject, RTCSessionDescriptionDelegate { setAssociatedObject(peerConnection, value: self, associativeKey: &sessionSetTarget) } - func peerConnection(peerConnection: RTCPeerConnection!, didCreateSessionDescription sdp: RTCSessionDescription!, error: NSError!) { + func peerConnection(_ peerConnection: RTCPeerConnection!, didCreateSessionDescription sdp: RTCSessionDescription!, error: Error!) { } - func peerConnection(peerConnection: RTCPeerConnection!, didSetSessionDescriptionWithError error: NSError!) { + func peerConnection(_ peerConnection: RTCPeerConnection!, didSetSessionDescriptionWithError error: Error!) { setAssociatedObject(peerConnection, value: "", associativeKey: &sessionSetTarget) @@ -109,7 +109,7 @@ extension RTCPeerConnection { } } - private func intDelegate() -> AAPeerConnectionDelegate { + fileprivate func intDelegate() -> AAPeerConnectionDelegate { let stored = self.delegate as? AAPeerConnectionDelegate if (stored != nil) { return stored! @@ -121,20 +121,20 @@ extension RTCPeerConnection { return nDelegate } - func createAnswer(constraints: RTCMediaConstraints, didCreate: (RTCSessionDescription!, NSError!) -> ()) { - createAnswerWithDelegate(AASessionDescriptionCreateDelegate(didCreate: didCreate, peerConnection: self), constraints: constraints) + func createAnswer(_ constraints: RTCMediaConstraints, didCreate: @escaping (RTCSessionDescription?, Error?) -> ()) { + self.createAnswer(with: AASessionDescriptionCreateDelegate(didCreate: didCreate, peerConnection: self), constraints: constraints) } - func createOffer(constraints: RTCMediaConstraints, didCreate: (RTCSessionDescription!, NSError!) -> ()) { - createOfferWithDelegate(AASessionDescriptionCreateDelegate(didCreate: didCreate, peerConnection: self), constraints: constraints) + func createOffer(_ constraints: RTCMediaConstraints, didCreate: @escaping (RTCSessionDescription?, Error?) -> ()) { + self.createOffer(with: AASessionDescriptionCreateDelegate(didCreate: didCreate, peerConnection: self), constraints: constraints) } - func setLocalDescription(sdp: RTCSessionDescription, didSet: (NSError!) -> ()) { - setLocalDescriptionWithDelegate(AASessionDescriptionSetDelegate(didSet: didSet, peerConnection: self), sessionDescription: sdp) + func setLocalDescription(_ sdp: RTCSessionDescription, didSet: @escaping (Error!) -> ()) { + setLocalDescriptionWith(AASessionDescriptionSetDelegate(didSet: didSet, peerConnection: self), sessionDescription: sdp) } - func setRemoteDescription(sdp: RTCSessionDescription, didSet: (NSError!) -> ()) { - setRemoteDescriptionWithDelegate(AASessionDescriptionSetDelegate(didSet: didSet, peerConnection: self), sessionDescription: sdp) + func setRemoteDescription(_ sdp: RTCSessionDescription, didSet: @escaping (Error!) -> ()) { + setRemoteDescriptionWith(AASessionDescriptionSetDelegate(didSet: didSet, peerConnection: self), sessionDescription: sdp) } } diff --git a/actor-sdk/sdk-core-ios/Podfile b/actor-sdk/sdk-core-ios/Podfile index 16b7e2440b..149cff30ba 100644 --- a/actor-sdk/sdk-core-ios/Podfile +++ b/actor-sdk/sdk-core-ios/Podfile @@ -12,8 +12,9 @@ target 'ActorApp' do # Core Tools pod 'RegexKitLite' - pod 'CocoaAsyncSocket' + # pod 'CocoaAsyncSocket' pod 'zipzap' + pod 'ReachabilitySwift', '~> 3' # Main UI pod 'TTTAttributedLabel' @@ -45,8 +46,9 @@ target 'ActorSDK' do # Core Tools pod 'RegexKitLite' - pod 'CocoaAsyncSocket' + # pod 'CocoaAsyncSocket' pod 'zipzap' + pod 'ReachabilitySwift', '~> 3' # Main UI pod 'TTTAttributedLabel' diff --git a/actor-sdk/sdk-core-ios/VERSION b/actor-sdk/sdk-core-ios/VERSION new file mode 100644 index 0000000000..415b19fc36 --- /dev/null +++ b/actor-sdk/sdk-core-ios/VERSION @@ -0,0 +1 @@ +2.0 \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-android/build.gradle b/actor-sdk/sdk-core/core/core-android/build.gradle index e5012203aa..390b823d44 100644 --- a/actor-sdk/sdk-core/core/core-android/build.gradle +++ b/actor-sdk/sdk-core/core/core-android/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:2.1.2' + classpath 'com.android.tools.build:gradle:2.1.3' classpath 'me.tatarka:gradle-retrolambda:3.2.5' } } diff --git a/actor-sdk/sdk-core/core/core-android/src/main/java/im/actor/core/AndroidMessenger.java b/actor-sdk/sdk-core/core/core-android/src/main/java/im/actor/core/AndroidMessenger.java index a78aae7ce7..7505a2d4ad 100644 --- a/actor-sdk/sdk-core/core/core-android/src/main/java/im/actor/core/AndroidMessenger.java +++ b/actor-sdk/sdk-core/core/core-android/src/main/java/im/actor/core/AndroidMessenger.java @@ -37,8 +37,8 @@ import im.actor.core.utils.AppStateActor; import im.actor.core.utils.IOUtils; import im.actor.core.utils.ImageHelper; +import im.actor.core.viewmodel.AppStateVM; import im.actor.core.viewmodel.Command; -import im.actor.core.viewmodel.CommandCallback; import im.actor.core.viewmodel.GalleryVM; import im.actor.runtime.Runtime; import im.actor.runtime.actors.Actor; @@ -49,8 +49,6 @@ import im.actor.runtime.android.AndroidContext; import im.actor.runtime.eventbus.EventBus; import im.actor.runtime.generic.mvvm.BindedDisplayList; -import im.actor.runtime.mvvm.Value; -import im.actor.runtime.mvvm.ValueChangedListener; import im.actor.core.utils.GalleryScannerActor; import me.leolin.shortcutbadger.ShortcutBadger; @@ -91,7 +89,7 @@ public void onChange(boolean selfChange) { })); // Counters - modules.getAppStateModule() + modules.getConductor() .getGlobalStateVM() .getGlobalCounter() .subscribe((val, valueModel) -> { @@ -311,6 +309,10 @@ public void sendVideo(Peer peer, String fullFilePath, String fileName) { } public Command sendUri(final Peer peer, final Uri uri) { + return sendUri(peer, uri, "Actor"); + } + + public Command sendUri(final Peer peer, final Uri uri, String appName) { return callback -> fileDownloader.execute(() -> { String[] filePathColumn = {MediaStore.Images.Media.DATA, MediaStore.Video.Media.MIME_TYPE, MediaStore.Video.Media.TITLE}; @@ -318,27 +320,31 @@ public Command sendUri(final Peer peer, final Uri uri) { String mimeType; String fileName; + String ext = ""; + Cursor cursor = context.getContentResolver().query(uri, filePathColumn, null, null, null); if (cursor != null) { cursor.moveToFirst(); picturePath = cursor.getString(cursor.getColumnIndex(filePathColumn[0])); mimeType = cursor.getString(cursor.getColumnIndex(filePathColumn[1])); fileName = cursor.getString(cursor.getColumnIndex(filePathColumn[2])); - if (mimeType == null) { - mimeType = "?/?"; - } cursor.close(); } else { picturePath = uri.getPath(); fileName = new File(uri.getPath()).getName(); int index = fileName.lastIndexOf("."); if (index > 0) { - mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileName.substring(index + 1)); + ext = fileName.substring(index + 1); + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext); } else { mimeType = "?/?"; } } + if (mimeType == null) { + mimeType = "?/?"; + } + if (picturePath == null || !uri.getScheme().equals("file")) { File externalFile = context.getExternalFilesDir(null); if (externalFile == null) { @@ -347,11 +353,16 @@ public Command sendUri(final Peer peer, final Uri uri) { } String externalPath = externalFile.getAbsolutePath(); - File dest = new File(externalPath + "/Actor/"); + File dest = new File(externalPath + "/" + + appName + + "/"); dest.mkdirs(); - boolean isGif = picturePath != null && picturePath.endsWith(".gif"); - File outputFile = new File(dest, "upload_" + random.nextLong() + (isGif ? ".gif" : ".jpg")); + if (ext.isEmpty() && picturePath != null) { + int index = picturePath.lastIndexOf("."); + ext = picturePath.substring(index + 1); + } + File outputFile = new File(dest, "upload_" + random.nextLong() + "." + ext); picturePath = outputFile.getAbsolutePath(); try { @@ -367,6 +378,8 @@ public Command sendUri(final Peer peer, final Uri uri) { fileName = picturePath; } + if (!ext.isEmpty() && !fileName.endsWith(ext)) + fileName += "." + ext; if (mimeType.startsWith("video/")) { sendVideo(peer, picturePath, fileName); // trackVideoSend(peer); @@ -509,6 +522,13 @@ public BindedDisplayList getDocsDisplayList(final Peer peer) { public GalleryVM getGalleryVM() { if (galleryVM == null) { galleryVM = new GalleryVM(); + checkGalleryScannerActor(); + } + return galleryVM; + } + + protected void checkGalleryScannerActor() { + if (galleryScannerActor == null) { galleryScannerActor = ActorSystem.system().actorOf(Props.create(new ActorCreator() { @Override public Actor create() { @@ -516,10 +536,10 @@ public Actor create() { } }), "actor/gallery_scanner"); } - return galleryVM; } public ActorRef getGalleryScannerActor() { + checkGalleryScannerActor(); return galleryScannerActor; } @@ -527,4 +547,12 @@ public EventBus getEvents() { return modules.getEvents(); } + public AppStateVM getAppStateVM() { + return modules.getConductor().getAppStateVM(); + } + + public void startImport() { + modules.getContactsModule().startImport(); + } + } \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/JsFacade.java b/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/JsFacade.java index d51189299f..b10a810aa4 100644 --- a/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/JsFacade.java +++ b/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/JsFacade.java @@ -448,17 +448,9 @@ public JsPromise editMessage(JsPeer peer, String id, String newText) { return JsPromise.create(new JsPromiseExecutor() { @Override public void execute() { - messenger.updateMessage(peer.convert(), newText, Long.parseLong(id)).start(new CommandCallback() { - @Override - public void onResult(Void res) { - resolve(); - } - - @Override - public void onError(Exception e) { - reject(e.getMessage()); - } - }); + messenger.updateMessage(peer.convert(), newText, Long.parseLong(id)) + .then(r -> resolve()) + .failure(e -> reject(e.getMessage())); } }); } @@ -1233,39 +1225,39 @@ private JsArray convertSearchRes(List>() { - @Override - public void onResult(List res) { - Log.d(TAG, "findGroups:result"); - JsArray jsRes = JsArray.createArray().cast(); - for (PeerSearchEntity s : res) { - if (s.getPeer().getPeerType() == PeerType.GROUP) { - jsRes.push(JsPeerSearchResult.create(messenger.buildPeerInfo(s.getPeer()), - s.getDescription(), s.getMembersCount(), (int) (s.getDate() / 1000L), - messenger.buildPeerInfo(Peer.user(s.getCreatorUid())), s.isPublic(), - s.isJoined())); - } else if (s.getPeer().getPeerType() == PeerType.PRIVATE) { - jsRes.push(JsPeerSearchResult.create(messenger.buildPeerInfo(s.getPeer()))); - } - // jsRes.push(); - } - resolve(jsRes); - } - - @Override - public void onError(Exception e) { - Log.d(TAG, "findGroups:error"); - reject(e.getMessage()); - } - }); - } - }); - } +// @UsedByApp +// public JsPromise findGroups() { +// return JsPromise.create(new JsPromiseExecutor() { +// @Override +// public void execute() { +// messenger.findPeers(PeerSearchType.GROUPS).start(new CommandCallback>() { +// @Override +// public void onResult(List res) { +// Log.d(TAG, "findGroups:result"); +// JsArray jsRes = JsArray.createArray().cast(); +// for (PeerSearchEntity s : res) { +// if (s.getPeer().getPeerType() == PeerType.GROUP) { +// jsRes.push(JsPeerSearchResult.create(messenger.buildPeerInfo(s.getPeer()), +// s.getDescription(), s.getMembersCount(), (int) (s.getDate() / 1000L), +// messenger.buildPeerInfo(Peer.user(s.getCreatorUid())), s.isPublic(), +// s.isJoined())); +// } else if (s.getPeer().getPeerType() == PeerType.PRIVATE) { +// jsRes.push(JsPeerSearchResult.create(messenger.buildPeerInfo(s.getPeer()))); +// } +// // jsRes.push(); +// } +// resolve(jsRes); +// } +// +// @Override +// public void onError(Exception e) { +// Log.d(TAG, "findGroups:error"); +// reject(e.getMessage()); +// } +// }); +// } +// }); +// } @UsedByApp public void changeMyAvatar(final JsFile file) { @@ -1329,43 +1321,19 @@ public JsPromise editGroupTitle(final int gid, final String newTitle) { return JsPromise.create(new JsPromiseExecutor() { @Override public void execute() { - //noinspection ConstantConditions - messenger.editGroupTitle(gid, newTitle).start(new CommandCallback() { - @Override - public void onResult(Void res) { - Log.d(TAG, "editGroupTitle:result"); - resolve(); - } - @Override - public void onError(Exception e) { - Log.d(TAG, "editGroupTitle:error"); - reject(e.getMessage()); - } - }); + messenger.editGroupTitle(gid, newTitle) + .then(r -> resolve()) + .failure(e -> reject(e.getMessage())); } + }); } + @UsedByApp public JsPromise editGroupAbout(final int gid, final String newAbout) { - return JsPromise.create(new JsPromiseExecutor() { - @Override - public void execute() { - messenger.editGroupAbout(gid, newAbout).start(new CommandCallback() { - @Override - public void onResult(Void res) { - resolve(); - } - - @Override - public void onError(Exception e) { - Log.e(TAG, e); - reject(e.getMessage()); - } - }); - } - }); + return JsPromise.from(messenger.editGroupAbout(gid, newAbout).map(r -> null)); } @UsedByApp @@ -1386,19 +1354,9 @@ public JsPromise createGroup(final String title, final JsFile file, final int[] public void execute() { String avatarDescriptor = file != null ? provider.registerUploadFile(file) : null; //noinspection ConstantConditions - messenger.createGroup(title, avatarDescriptor, uids).start(new CommandCallback() { - @Override - public void onResult(Integer res) { - Log.d(TAG, "createGroup:result"); - resolve(JsPeer.create(Peer.group(res))); - } - - @Override - public void onError(Exception e) { - Log.d(TAG, "createGroup:error"); - reject(e.getMessage()); - } - }); + messenger.createGroup(title, avatarDescriptor, uids) + .then(r -> resolve(JsPeer.create(Peer.group(r)))) + .failure(e -> reject(e.getMessage())); } }); } @@ -1451,25 +1409,7 @@ public void onError(Exception e) { @UsedByApp public JsPromise leaveGroup(final int gid) { - return JsPromise.create(new JsPromiseExecutor() { - @Override - public void execute() { - //noinspection ConstantConditions - messenger.leaveGroup(gid).start(new CommandCallback() { - @Override - public void onResult(Void res) { - Log.d(TAG, "leaveGroup:result"); - resolve(); - } - - @Override - public void onError(Exception e) { - Log.d(TAG, "leaveGroup:error"); - reject(e.getMessage()); - } - }); - } - }); + return JsPromise.from(messenger.leaveGroup(gid).map(r -> null)); } @UsedByApp diff --git a/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/entity/JsContent.java b/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/entity/JsContent.java index cb2c36ea2e..a527c9552c 100644 --- a/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/entity/JsContent.java +++ b/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/entity/JsContent.java @@ -30,6 +30,7 @@ public abstract class JsContent extends JavaScriptObject { public static JsContent createContent(AbsContent src, int sender) { + JsMessenger messenger = JsMessenger.getInstance(); JsContent content; if (src instanceof TextContent) { @@ -58,7 +59,7 @@ public static JsContent createContent(AbsContent src, int sender) { content = JsContentText.create(((TextContent) src).getText()); } } else if (src instanceof ServiceContent) { - content = JsContentService.create(messenger.getFormatter().formatFullServiceMessage(sender, (ServiceContent) src)); + content = JsContentService.create(messenger.getFormatter().formatFullServiceMessage(sender, (ServiceContent) src, false)); } else if (src instanceof DocumentContent) { DocumentContent doc = (DocumentContent) src; diff --git a/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/entity/JsDialog.java b/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/entity/JsDialog.java index caf0e9a2df..337ee7494a 100644 --- a/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/entity/JsDialog.java +++ b/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/entity/JsDialog.java @@ -8,6 +8,8 @@ import im.actor.core.entity.ContentType; import im.actor.core.entity.Dialog; +import im.actor.core.entity.GroupType; +import im.actor.core.entity.Peer; import im.actor.core.entity.PeerType; import im.actor.core.js.JsMessenger; import im.actor.runtime.js.mvvm.JsEntityConverter; @@ -22,7 +24,8 @@ public JsDialog convert(Dialog src) { JsMessenger messenger = JsMessenger.getInstance(); boolean showSender = false; - if (src.getPeer().getPeerType() == PeerType.GROUP) { + Peer peer = src.getPeer(); + if (peer.getPeerType() == PeerType.GROUP) { if (src.getMessageType() != ContentType.SERVICE && src.getMessageType() != ContentType.NONE) { showSender = src.getSenderId() != 0; } @@ -40,12 +43,14 @@ public JsDialog convert(Dialog src) { fileUrl = messenger.getFileUrl(src.getDialogAvatar().getSmallImage().getFileReference()); } + boolean isChannel = peer.getPeerType() == PeerType.GROUP && messenger.getGroups().get(peer.getPeerId()).getGroupType() == GroupType.CHANNEL; + boolean highlightContent = src.getMessageType() != ContentType.TEXT; String messageText = messenger.getFormatter().formatContentText(src.getSenderId(), - src.getMessageType(), src.getText(), src.getRelatedUid()); + src.getMessageType(), src.getText(), src.getRelatedUid(), false); - JsPeerInfo peerInfo = JsPeerInfo.create(JsPeer.create(src.getPeer()), src.getDialogTitle(), null, fileUrl, - Placeholders.getPlaceholder(src.getPeer().getPeerId()), false); + JsPeerInfo peerInfo = JsPeerInfo.create(JsPeer.create(peer), src.getDialogTitle(), null, fileUrl, + Placeholders.getPlaceholder(peer.getPeerId()), isChannel); String state = "unknown"; if (messenger.myUid() == src.getSenderId()) { diff --git a/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/entity/JsGroup.java b/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/entity/JsGroup.java index b61dff0bc5..9bccd9b11a 100644 --- a/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/entity/JsGroup.java +++ b/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/entity/JsGroup.java @@ -6,6 +6,7 @@ import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray; + import im.actor.core.entity.Avatar; import im.actor.core.entity.GroupMember; import im.actor.core.entity.Peer; @@ -21,7 +22,7 @@ public class JsGroup extends JavaScriptObject { public static JsGroup fromGroupVM(GroupVM groupVM, JsMessenger messenger) { int online = groupVM.getPresence().get(); - String presence = messenger.getFormatter().formatGroupMembers(groupVM.getMembersCount()); + String presence = messenger.getFormatter().formatGroupMembers(groupVM.getMembersCount().get()); if (online > 0) { presence += ", " + messenger.getFormatter().formatGroupOnline(online); } @@ -45,7 +46,7 @@ public static JsGroup fromGroupVM(GroupVM groupVM, JsMessenger messenger) { // Log.d("JsGroup", "PeerInfo: " + peerInfo); convertedMembers.add(JsGroupMember.create(peerInfo, g.isAdministrator(), - g.getInviterUid() == messenger.myUid() || groupVM.getCreatorId() == messenger.myUid())); + g.getInviterUid() == messenger.myUid())); } Collections.sort(convertedMembers, new Comparator() { @Override @@ -58,16 +59,16 @@ public int compare(JsGroupMember o1, JsGroupMember o2) { jsMembers.push(member); } return create(groupVM.getId(), groupVM.getName().get(), groupVM.getAbout().get(), fileUrl, bigFileUrl, - Placeholders.getPlaceholder(groupVM.getId()), groupVM.getCreatorId(), presence, + Placeholders.getPlaceholder(groupVM.getId()), presence, jsMembers); } public static native JsGroup create(int id, String name, String about, String avatar, String bigAvatar, - String placeholder, int adminId, String presence, + String placeholder, String presence, JsArray members)/*-{ return { id: id, name: name, about: about, avatar: avatar, bigAvatar: bigAvatar, placeholder: placeholder, - adminId: adminId, presence: presence, members: members + presence: presence, members: members }; }-*/; diff --git a/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/modules/JsBindingModule.java b/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/modules/JsBindingModule.java index bf33db3ca4..f257742787 100644 --- a/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/modules/JsBindingModule.java +++ b/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/modules/JsBindingModule.java @@ -165,7 +165,7 @@ public void onChanged(ArrayList val, Value> public JsBindedValue getOnlineStatus() { if (onlineState == null) { - final GlobalStateVM vm = context().getAppStateModule().getGlobalStateVM(); + final GlobalStateVM vm = context().getConductor().getGlobalStateVM(); onlineState = new JsBindedValue<>("online"); vm.getIsConnecting().subscribe(new ValueChangedListener() { @@ -307,11 +307,11 @@ public void onChanged(Integer val, Value valueModel) { value.changeValue(null); return; } - String presence = messenger.getFormatter().formatGroupMembers(groupVM.getMembersCount()); + String presence = messenger.getFormatter().formatGroupMembers(groupVM.getMembersCount().get()); if (val > 0) { presence += ", " + messenger.getFormatter().formatGroupOnline(val); } - value.changeValue(JsOnlineGroup.create(groupVM.getMembersCount(), val, presence, false)); + value.changeValue(JsOnlineGroup.create(groupVM.getMembersCount().get(), val, presence, false)); } else { value.changeValue(JsOnlineGroup.create(0, 0, "Not member", false)); } @@ -430,7 +430,7 @@ public JsDisplayList getSharedMessageList(Peer peer) { public JsBindedValue getGlobalCounter() { if (globalCounter == null) { - ValueModel counter = context().getAppStateModule().getGlobalStateVM().getGlobalCounter(); + ValueModel counter = context().getConductor().getGlobalStateVM().getGlobalCounter(); globalCounter = new JsBindedValue<>(JsCounter.create(counter.get())); counter.subscribe(new ValueChangedListener() { @Override @@ -444,7 +444,7 @@ public void onChanged(Integer val, Value valueModel) { public JsBindedValue getTempGlobalCounter() { if (tempGlobalCounter == null) { - ValueModel counter = context().getAppStateModule().getGlobalStateVM().getGlobalTempCounter(); + ValueModel counter = context().getConductor().getGlobalStateVM().getGlobalTempCounter(); tempGlobalCounter = new JsBindedValue<>(JsCounter.create(counter.get())); counter.subscribe(new ValueChangedListener() { @Override diff --git a/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/modules/JsFilesModule.java b/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/modules/JsFilesModule.java index 65221f229b..82dd212b84 100644 --- a/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/modules/JsFilesModule.java +++ b/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/modules/JsFilesModule.java @@ -22,10 +22,8 @@ import im.actor.core.network.RpcException; import im.actor.runtime.Log; import im.actor.runtime.Storage; -import im.actor.runtime.actors.ActorCreator; +import im.actor.runtime.actors.ActorCancellable; import im.actor.runtime.actors.ActorRef; -import im.actor.runtime.actors.Cancellable; -import im.actor.runtime.actors.Props; import static im.actor.runtime.actors.ActorSystem.system; @@ -137,7 +135,7 @@ private static class FileBinderActor extends ModuleActor { private boolean isLoading = false; private JsFilesModule filesModule; private ArrayList filesQueue = new ArrayList<>(); - private Cancellable performCancellable; + private ActorCancellable performCancellable; public FileBinderActor(JsFilesModule filesModule, ModuleContext context) { super(context); diff --git a/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/modules/JsIdleModule.java b/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/modules/JsIdleModule.java index a4137ea121..0be53dc8fc 100644 --- a/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/modules/JsIdleModule.java +++ b/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/modules/JsIdleModule.java @@ -5,10 +5,8 @@ import im.actor.core.modules.AbsModule; import im.actor.core.modules.ModuleActor; import im.actor.core.modules.ModuleContext; -import im.actor.runtime.actors.ActorCreator; +import im.actor.runtime.actors.ActorCancellable; import im.actor.runtime.actors.ActorRef; -import im.actor.runtime.actors.Cancellable; -import im.actor.runtime.actors.Props; import static im.actor.runtime.actors.ActorSystem.system; @@ -41,7 +39,7 @@ private static class IdleActor extends ModuleActor implements JsIdleCallback { private boolean isAppVisible = true; private JsMessenger messenger; - private Cancellable flushCancellable; + private ActorCancellable flushCancellable; private boolean isElectron = JsElectronApp.isElectron(); public IdleActor(JsMessenger messenger, ModuleContext context) { diff --git a/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/providers/JsNotificationsProvider.java b/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/providers/JsNotificationsProvider.java index 4f719e6b5f..af087fd9a9 100644 --- a/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/providers/JsNotificationsProvider.java +++ b/actor-sdk/sdk-core/core/core-js/src/main/java/im/actor/core/js/providers/JsNotificationsProvider.java @@ -10,7 +10,9 @@ import im.actor.core.Messenger; import im.actor.core.entity.Avatar; +import im.actor.core.entity.GroupType; import im.actor.core.entity.Notification; +import im.actor.core.entity.Peer; import im.actor.core.entity.PeerType; import im.actor.core.js.entity.JsPeer; import im.actor.core.js.JsMessenger; @@ -50,15 +52,16 @@ public void onNotification(Messenger messenger, List topNotificati Notification notification = topNotifications.get(0); // Peer info + Peer peer = notification.getPeer(); if (conversationsCount == 1) { Avatar peerAvatar; - JsPeer jsPeer = JsPeer.create(notification.getPeer()); - if (notification.getPeer().getPeerType() == PeerType.PRIVATE) { - UserVM userVM = messenger.getUser(notification.getPeer().getPeerId()); + JsPeer jsPeer = JsPeer.create(peer); + if (peer.getPeerType() == PeerType.PRIVATE) { + UserVM userVM = messenger.getUser(peer.getPeerId()); peerTitle = userVM.getName().get(); peerAvatar = userVM.getAvatar().get(); } else { - GroupVM groupVM = messenger.getGroup(notification.getPeer().getPeerId()); + GroupVM groupVM = messenger.getGroup(peer.getPeerId()); peerTitle = groupVM.getName().get(); peerAvatar = groupVM.getAvatar().get(); } @@ -80,19 +83,22 @@ public void onNotification(Messenger messenger, List topNotificati showCounters = true; } + boolean isChannel = peer.getPeerType() == PeerType.GROUP && messenger.getGroups().get(peer.getPeerId()).getGroupType() == GroupType.CHANNEL; + if (conversationsCount == 1) { for (int i = 0; i < nCount; i++) { Notification n = topNotifications.get(i); if (contentMessage.length() > 0) { contentMessage += "\n"; } - if (notification.getPeer().getPeerType() == PeerType.GROUP) { + if (peer.getPeerType() == PeerType.GROUP) { contentMessage += messenger.getUser(notification.getSender()).getName().get() + ": "; } contentMessage += messenger.getFormatter().formatContentText(n.getSender(), n.getContentDescription().getContentType(), n.getContentDescription().getText(), - n.getContentDescription().getRelatedUser()); + n.getContentDescription().getRelatedUser(), + isChannel); } if (showCounters) { @@ -114,7 +120,8 @@ public void onNotification(Messenger messenger, List topNotificati contentMessage += messenger.getFormatter().formatContentText(n.getSender(), n.getContentDescription().getContentType(), n.getContentDescription().getText(), - n.getContentDescription().getRelatedUser()); + n.getContentDescription().getRelatedUser(), + isChannel); } if (showCounters) { diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/AutoJoinType.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/AutoJoinType.java new file mode 100644 index 0000000000..1b7e81c47f --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/AutoJoinType.java @@ -0,0 +1,6 @@ +package im.actor.core; + +public enum AutoJoinType { + AFTER_INIT, + IMMEDIATELY +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/Configuration.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/Configuration.java index cc46e76e87..ddd67f9317 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/Configuration.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/Configuration.java @@ -11,7 +11,6 @@ import im.actor.core.providers.PhoneBookProvider; import im.actor.core.providers.CallsProvider; import im.actor.runtime.mtproto.ConnectionEndpoint; -import im.actor.runtime.webrtc.WebRTCIceServer; /** * Configuration for Messenger @@ -57,9 +56,17 @@ public class Configuration { @Property("readonly, nonatomic") private final boolean enablePhoneBookImport; @Property("readonly, nonatomic") + private final boolean enableOnClientPrivacy; + @Property("readonly, nonatomic") private final CallsProvider callsProvider; @Property("readonly, nonatomic") + private final RawUpdatesHandler rawUpdatesHandler; + @Property("readonly, nonatomic") private final boolean isEnabledGroupedChatList; + @Property("readonly, nonatomic") + private final String[] autoJoinGroups; + @Property("readonly, nonatomic") + private final AutoJoinType autoJoinType; Configuration(ConnectionEndpoint[] endpoints, PhoneBookProvider phoneBookProvider, @@ -78,10 +85,14 @@ public class Configuration { String customAppName, TrustedKey[] trustedKeys, boolean enablePhoneBookImport, + boolean enableOnClientPrivcy, CallsProvider callsProvider, + RawUpdatesHandler rawUpdatesHandler, boolean voiceCallsEnabled, boolean videoCallsEnabled, - boolean isEnabledGroupedChatList) { + boolean isEnabledGroupedChatList, + String[] autoJoinGroups, + AutoJoinType autoJoinType) { this.endpoints = endpoints; this.phoneBookProvider = phoneBookProvider; this.enableContactsLogging = enableContactsLogging; @@ -99,10 +110,14 @@ public class Configuration { this.customAppName = customAppName; this.trustedKeys = trustedKeys; this.enablePhoneBookImport = enablePhoneBookImport; + this.enableOnClientPrivacy = enableOnClientPrivcy; this.callsProvider = callsProvider; + this.rawUpdatesHandler = rawUpdatesHandler; this.voiceCallsEnabled = voiceCallsEnabled; this.videoCallsEnabled = videoCallsEnabled; this.isEnabledGroupedChatList = isEnabledGroupedChatList; + this.autoJoinGroups = autoJoinGroups; + this.autoJoinType = autoJoinType; } /** @@ -132,6 +147,15 @@ public CallsProvider getCallsProvider() { return callsProvider; } + /** + * Getting RawUpdatesHandler if set + * + * @return raw updates handler + */ + public RawUpdatesHandler getRawUpdatesHandler() { + return rawUpdatesHandler; + } + /** * Getting if app automatically imports phone book to server * @@ -141,6 +165,15 @@ public boolean isEnablePhoneBookImport() { return enablePhoneBookImport; } + /** + * Getting if app check if contact not in phone book and hides phone/email in that case + * + * @return if on client privacy enabled + */ + public boolean isEnableOnClientPrivacy() { + return enableOnClientPrivacy; + } + /** * Getting Trusted keys * @@ -293,4 +326,22 @@ public String[] getPreferredLanguages() { public boolean isEnabledGroupedChatList() { return isEnabledGroupedChatList; } + + /** + * Get Auto Join groups + * + * @return list of auto join groups + */ + public String[] getAutoJoinGroups() { + return autoJoinGroups; + } + + /** + * Get Auto Join Type + * + * @return auto join type + */ + public AutoJoinType getAutoJoinType() { + return autoJoinType; + } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/ConfigurationBuilder.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/ConfigurationBuilder.java index 6a9971074f..0f82c000c7 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/ConfigurationBuilder.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/ConfigurationBuilder.java @@ -19,6 +19,7 @@ import im.actor.runtime.Crypto; import im.actor.runtime.Log; import im.actor.runtime.mtproto.ConnectionEndpoint; +import im.actor.runtime.mtproto.ConnectionEndpointArray; import im.actor.runtime.util.Hex; import im.actor.runtime.webrtc.WebRTCIceServer; @@ -28,7 +29,7 @@ public class ConfigurationBuilder { private ArrayList trustedKeys = new ArrayList<>(); - private ArrayList endpoints = new ArrayList<>(); + private ConnectionEndpointArray endpoints = new ConnectionEndpointArray(); private PhoneBookProvider phoneBookProvider; @@ -57,11 +58,29 @@ public class ConfigurationBuilder { private String customAppName; private boolean isPhoneBookImportEnabled = true; + private boolean isOnClientPrivacyEnabled = false; private CallsProvider callsProvider; + private RawUpdatesHandler rawUpdatesHandler; private boolean isEnabledGroupedChatList = true; + private ArrayList autoJoinGroups = new ArrayList<>(); + private AutoJoinType autoJoinType = AutoJoinType.AFTER_INIT; + + /** + * Setting Auto Join to group type: when to join to your groups + * + * @param autoJoinType auto join type + * @return this + */ + @ObjectiveCName("setAutoJoinType:") + public ConfigurationBuilder setAutoJoinType(AutoJoinType autoJoinType) { + this.autoJoinType = autoJoinType; + return this; + } + + /** * Setting if grouped chat list support enabled * @@ -111,6 +130,19 @@ public ConfigurationBuilder setPhoneBookImportEnabled(boolean isPhoneBookImportE return this; } + /** + * Setting if application uses on client contacts privacy + * + * @param isOnClientPrivacyEnabled enabled flag + * @return this + */ + @NotNull + @ObjectiveCName("setOnClientPrivacyEnabled:") + public ConfigurationBuilder setOnClientPrivacyEnabled(boolean isOnClientPrivacyEnabled) { + this.isOnClientPrivacyEnabled = isOnClientPrivacyEnabled; + return this; + } + /** * Setting Web RTC support provider * @@ -124,6 +156,19 @@ public ConfigurationBuilder setCallsProvider(CallsProvider callsProvider) { return this; } + /** + * Setting raw updates handler + * + * @param rawUpdatesHandler raw updates handler + * @return this + */ + @NotNull + @ObjectiveCName("setRawUpdatesHandler:") + public ConfigurationBuilder setRawUpdatesHandler(RawUpdatesHandler rawUpdatesHandler) { + this.rawUpdatesHandler = rawUpdatesHandler; + return this; + } + /** * Adding Trusted key for protocol encryption securing * @@ -137,6 +182,19 @@ public ConfigurationBuilder addTrustedKey(String trustedKey) { return this; } + /** + * Adding group to auto join of users + * + * @param groupTokenOrShortName group's token or short name + * @return this + */ + @NotNull + @ObjectiveCName("addAutoJoinGroupWithToken:") + public ConfigurationBuilder addAutoJoinGroup(String groupTokenOrShortName) { + autoJoinGroups.add(groupTokenOrShortName); + return this; + } + /** * Setting custom application name * @@ -332,51 +390,10 @@ public ConfigurationBuilder setMaxFailureCount(int maxFailureCount) { @NotNull @ObjectiveCName("addEndpoint:") public ConfigurationBuilder addEndpoint(@NotNull String url) { - - // Manual buggy parsing for GWT - // TODO: Correct URL parsing - String scheme = url.substring(0, url.indexOf(":")).toLowerCase(); - String host = url.substring(url.indexOf("://") + "://".length()); - String knownIp = null; - - if (host.endsWith("/")) { - host = host.substring(0, host.length() - 1); - } - int port = -1; - if (host.contains(":")) { - String[] parts = host.split(":"); - host = parts[0]; - port = Integer.parseInt(parts[1]); - } - - if (host.contains("@")) { - String[] parts = host.split("@"); - host = parts[0]; - knownIp = parts[1]; - } - - if (scheme.equals("ssl") || scheme.equals("tls")) { - if (port <= 0) { - port = 443; - } - endpoints.add(new ConnectionEndpoint(host, port, knownIp, ConnectionEndpoint.TYPE_TCP_TLS)); - } else if (scheme.equals("tcp")) { - if (port <= 0) { - port = 80; - } - endpoints.add(new ConnectionEndpoint(host, port, knownIp, ConnectionEndpoint.TYPE_TCP)); - } else if (scheme.equals("ws")) { - if (port <= 0) { - port = 80; - } - endpoints.add(new ConnectionEndpoint(host, port, knownIp, ConnectionEndpoint.TYPE_WS)); - } else if (scheme.equals("wss")) { - if (port <= 0) { - port = 443; - } - endpoints.add(new ConnectionEndpoint(host, port, knownIp, ConnectionEndpoint.TYPE_WS_TLS)); - } else { - throw new RuntimeException("Unknown scheme type: " + scheme); + try { + endpoints.addEndpoint(url); + } catch (ConnectionEndpointArray.UnknownSchemeException e) { + throw new RuntimeException(e.getMessage()); } return this; } @@ -416,9 +433,13 @@ public Configuration build() { customAppName, trustedKeys.toArray(new TrustedKey[trustedKeys.size()]), isPhoneBookImportEnabled, + isOnClientPrivacyEnabled, callsProvider, + rawUpdatesHandler, voiceCallsEnabled, videoCallsEnabled, - isEnabledGroupedChatList); + isEnabledGroupedChatList, + autoJoinGroups.toArray(new String[autoJoinGroups.size()]), + autoJoinType); } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/Messenger.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/Messenger.java index 743e975f7c..dcf2003780 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/Messenger.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/Messenger.java @@ -21,11 +21,14 @@ import im.actor.core.entity.AuthStartRes; import im.actor.core.entity.FileReference; import im.actor.core.entity.Group; +import im.actor.core.entity.GroupMembersSlice; +import im.actor.core.entity.GroupPermissions; import im.actor.core.entity.MentionFilterResult; import im.actor.core.entity.MessageSearchEntity; import im.actor.core.entity.Peer; import im.actor.core.entity.PeerSearchEntity; import im.actor.core.entity.PeerSearchType; +import im.actor.core.entity.SearchResult; import im.actor.core.entity.Sex; import im.actor.core.entity.User; import im.actor.core.entity.WebActionDescriptor; @@ -67,7 +70,9 @@ import im.actor.core.viewmodel.UserVM; import im.actor.runtime.actors.ActorSystem; import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.mtproto.ConnectionEndpointArray; import im.actor.runtime.mvvm.MVVMCollection; +import im.actor.runtime.mvvm.SearchValueModel; import im.actor.runtime.mvvm.ValueModel; import im.actor.runtime.promise.Promise; import im.actor.runtime.storage.PreferencesStorage; @@ -219,6 +224,20 @@ public Promise doCompleteAuth(AuthRes authRes) { return modules.getAuthModule().doCompleteAuth(authRes); } + /** + * Change endpoint + * + * @param endpoint endpoint to change to, null for reset to default + * @throws ConnectionEndpointArray.UnknownSchemeException + */ + public void changeEndpoint(String endpoint) throws ConnectionEndpointArray.UnknownSchemeException { + if (endpoint != null && !endpoint.isEmpty()) { + modules.getApiModule().changeEndpoint(endpoint); + } else { + modules.getApiModule().resetToDefaultEndpoints(); + } + } + /** * Request email auth * @@ -394,7 +413,7 @@ public void onLoggedIn() { @NotNull @ObjectiveCName("getAppState") public AppStateVM getAppState() { - return modules.getAppStateModule().getAppStateVM(); + return modules.getConductor().getAppStateVM(); } /** @@ -405,7 +424,7 @@ public AppStateVM getAppState() { @NotNull @ObjectiveCName("getGlobalState") public GlobalStateVM getGlobalState() { - return modules.getAppStateModule().getGlobalStateVM(); + return modules.getConductor().getGlobalStateVM(); } /** @@ -671,12 +690,13 @@ public void onNetworkChanged(@NotNull NetworkState state) { /** * MUST be called when external push received * - * @param seq sequence number of update + * @param seq sequence number of update + * @param authId auth id */ - @ObjectiveCName("onPushReceivedWithSeq:") - public void onPushReceived(int seq) { + @ObjectiveCName("onPushReceivedWithSeq:withAuthId:") + public void onPushReceived(int seq, long authId) { if (modules.getUpdatesModule() != null) { - modules.getUpdatesModule().onPushReceived(seq); + modules.getUpdatesModule().onPushReceived(seq, authId); } } @@ -1100,7 +1120,18 @@ public String loadDraft(Peer peer) { @ObjectiveCName("loadLastMessageDate:") @Deprecated public long loadLastMessageDate(Peer peer) { - return getConversationVM(peer).getLastMessageDate(); + return getConversationVM(peer).getLastReadMessageDate(); + } + + /** + * Finding public by id + * + * @param gid group id + * @return found peer promise + */ + @ObjectiveCName("findPublicGroupByIdWithGid:") + public Promise findPublicGroupById(int gid) { + return modules.getSearchModule().findPublicGroupById(gid); } /** @@ -1128,6 +1159,19 @@ public Command> findPeers(PeerSearchType type) { .failure(e -> callback.onError(e)); } + /** + * Finding peers by text query + * + * @param query text query + * @return found peers + */ + @ObjectiveCName("findPeersWithQuery:") + public Command> findPeers(String query) { + return callback -> modules.getSearchModule().findPeers(query) + .then(v -> callback.onResult(v)) + .failure(e -> callback.onError(e)); + } + /** * Finding text messages by query * @@ -1181,6 +1225,16 @@ public Command> findAllPhotos(Peer peer) { .failure(e -> callback.onError(e)); } + /** + * Building global search model + * + * @return search model + */ + @ObjectiveCName("buildGlobalSearchModel") + public SearchValueModel buildGlobalSearchModel() { + return modules.getSearchModule().buildSearchModel(); + } + ////////////////////////////////////// // Calls ////////////////////////////////////// @@ -1386,14 +1440,12 @@ public Command editName(final int uid, final String name) { * * @param gid group's id * @param title new group title - * @return Command for execution + * @return Promise for void */ - @Nullable - @ObjectiveCName("editGroupTitleCommandWithGid:withTitle:") - public Command editGroupTitle(final int gid, final String title) { - return callback -> modules.getGroupsModule().editTitle(gid, title) - .then(v -> callback.onResult(v)) - .failure(e -> callback.onError(e)); + @NotNull + @ObjectiveCName("editGroupTitleWithGid:withTitle:") + public Promise editGroupTitle(final int gid, final String title) { + return modules.getGroupsModule().editTitle(gid, title); } /** @@ -1416,14 +1468,50 @@ public Command editGroupTheme(final int gid, final String theme) { * * @param gid group's id * @param about new group about - * @return Command for execution + * @return Promise for void */ @NotNull - @ObjectiveCName("editGroupAboutCommandWithGid:withAbout:") - public Command editGroupAbout(final int gid, final String about) { - return callback -> modules.getGroupsModule().editAbout(gid, about) - .then(v -> callback.onResult(v)) - .failure(e -> callback.onError(e)); + @ObjectiveCName("editGroupAboutWithGid:withAbout:") + public Promise editGroupAbout(int gid, String about) { + return modules.getGroupsModule().editAbout(gid, about); + } + + /** + * Edit group's short name + * + * @param gid group's id + * @param shortName new group short name + * @return Promise for void + */ + @NotNull + @ObjectiveCName("editGroupShortNameWithGid:withAbout:") + public Promise editGroupShortName(int gid, String shortName) { + return modules.getGroupsModule().editShortName(gid, shortName); + } + + /** + * Load Group's permissions + * + * @param gid group's id + * @return promise of permissions + */ + @NotNull + @ObjectiveCName("loadGroupPermissionsWithGid:") + public Promise loadGroupPermissions(int gid) { + return modules.getGroupsModule().loadAdminSettings(gid); + } + + /** + * Save Group's permissions + * + * @param gid group's id + * @param adminSettings settings + * @return promise of void + */ + @NotNull + @ObjectiveCName("saveGroupPermissionsWithGid:withSettings:") + public Promise saveGroupPermissions(int gid, GroupPermissions adminSettings) { + return modules.getGroupsModule().saveAdminSettings(gid, adminSettings); } /** @@ -1458,29 +1546,73 @@ public void removeGroupAvatar(int gid) { * @param title group title * @param avatarDescriptor descriptor of group avatar (can be null if not set) * @param uids member's ids - * @return Command for execution + * @return Promise of group id */ - @Nullable - @ObjectiveCName("createGroupCommandWithTitle:withAvatar:withUids:") - public Command createGroup(String title, String avatarDescriptor, int[] uids) { - return callback -> modules.getGroupsModule().createGroup(title, avatarDescriptor, uids) - .then(integer -> callback.onResult(integer)) - .failure(e -> callback.onError(e)); + @NotNull + @ObjectiveCName("createGroupWithTitle:withAvatar:withUids:") + public Promise createGroup(String title, String avatarDescriptor, int[] uids) { + return modules.getGroupsModule().createGroup(title, avatarDescriptor, uids); } + /** + * Create channel + * + * @param title channel title + * @param avatarDescriptor descriptor of channel avatar (can be null if not set) + * @return Promise of channel id + */ + @NotNull + @ObjectiveCName("createChannelWithTitle:withAvatar:") + public Promise createChannel(String title, String avatarDescriptor) { + return modules.getGroupsModule().createChannel(title, avatarDescriptor); + } /** * Leave group * * @param gid group's id - * @return Command for execution + * @return Promise of Void */ - @Nullable - @ObjectiveCName("leaveGroupCommandWithGid:") - public Command leaveGroup(final int gid) { - return callback -> modules.getGroupsModule().leaveGroup(gid) - .then(v -> callback.onResult(v)) - .failure(e -> callback.onError(e)); + @NotNull + @ObjectiveCName("leaveGroupWithGid:") + public Promise leaveGroup(final int gid) { + return modules.getGroupsModule().leaveGroup(gid); + } + + /** + * Leave and delete group + * + * @param gid group's id + * @return Promise of Void + */ + @NotNull + @ObjectiveCName("leaveAndDeleteGroupWithGid:") + public Promise leaveAndDeleteGroup(int gid) { + return modules.getGroupsModule().leaveAndDeleteGroup(gid); + } + + /** + * Delete Group + * + * @param gid group's id + * @return Promise of void + */ + @NotNull + @ObjectiveCName("deleteGroupWithGid:") + public Promise deleteGroup(int gid) { + return modules.getGroupsModule().deleteGroup(gid); + } + + /** + * Share Group History + * + * @param gid group's id + * @return Promise of void + */ + @NotNull + @ObjectiveCName("shareHistoryWithGid:") + public Promise shareHistory(int gid) { + return modules.getGroupsModule().shareHistory(gid); } /** @@ -1490,7 +1622,7 @@ public Command leaveGroup(final int gid) { * @param uid user's id * @return Command for execution */ - @Nullable + @NotNull @ObjectiveCName("inviteMemberCommandWithGid:withUid:") public Command inviteMember(int gid, int uid) { return callback -> modules.getGroupsModule().addMember(gid, uid) @@ -1498,6 +1630,19 @@ public Command inviteMember(int gid, int uid) { .failure(e -> callback.onError(e)); } + /** + * Adding member to group + * + * @param gid group's id + * @param uid user's id + * @return promise of adding member to group + */ + @NotNull + @ObjectiveCName("inviteMemberPromiseWithGid:withUid:") + public Promise inviteMemberPromise(int gid, int uid) { + return modules.getGroupsModule().addMember(gid, uid); + } + /** * Kick member from group * @@ -1505,7 +1650,7 @@ public Command inviteMember(int gid, int uid) { * @param uid user's id * @return Command for execution */ - @Nullable + @NotNull @ObjectiveCName("kickMemberCommandWithGid:withUid:") public Command kickMember(int gid, int uid) { return callback -> modules.getGroupsModule().kickMember(gid, uid) @@ -1513,6 +1658,19 @@ public Command kickMember(int gid, int uid) { .failure(e -> callback.onError(e)); } + /** + * Load async members + * + * @param gid group id + * @param limit limit of loading + * @param next optional cursor of next + * @return promise of members slice + */ + @ObjectiveCName("loadMembersWithGid:withLimit:withNext:") + public Promise loadMembers(int gid, int limit, byte[] next) { + return modules.getGroupsModule().loadMembers(gid, limit, next); + } + /** * Make member admin of group * @@ -1520,7 +1678,7 @@ public Command kickMember(int gid, int uid) { * @param uid user's id * @return Command for execution */ - @Nullable + @NotNull @ObjectiveCName("makeAdminCommandWithGid:withUid:") public Command makeAdmin(final int gid, final int uid) { return callback -> modules.getGroupsModule().makeAdmin(gid, uid) @@ -1528,13 +1686,41 @@ public Command makeAdmin(final int gid, final int uid) { .failure(e -> callback.onError(e)); } + /** + * Revoke member admin rights of group + * + * @param gid group's id + * @param uid user's id + * @return Command for execution + */ + @NotNull + @ObjectiveCName("revokeAdminCommandWithGid:withUid:") + public Command revokeAdmin(final int gid, final int uid) { + return callback -> modules.getGroupsModule().revokeAdmin(gid, uid) + .then(v -> callback.onResult(v)) + .failure(e -> callback.onError(e)); + } + + /** + * Transfer ownership of group + * + * @param gid group's id + * @param uid user's id + * @return Promise of void + */ + @NotNull + @ObjectiveCName("transferOwnershipWithGid:withUid:") + public Promise transferOwnership(int gid, int uid) { + return modules.getGroupsModule().transferOwnership(gid, uid); + } + /** * Request invite link for group * * @param gid group's id * @return Command for execution */ - @Nullable + @NotNull @ObjectiveCName("requestInviteLinkCommandWithGid:") public Command requestInviteLink(int gid) { return callback -> modules.getGroupsModule().requestInviteLink(gid) @@ -1548,7 +1734,7 @@ public Command requestInviteLink(int gid) { * @param gid group's id * @return Command for execution */ - @Nullable + @NotNull @ObjectiveCName("requestRevokeLinkCommandWithGid:") public Command revokeInviteLink(int gid) { return callback -> modules.getGroupsModule().requestRevokeLink(gid) @@ -1562,7 +1748,7 @@ public Command revokeInviteLink(int gid) { * @param token invite token * @return Command for execution */ - @Nullable + @NotNull @ObjectiveCName("joinGroupViaLinkCommandWithToken:") public Command joinGroupViaToken(String token) { return callback -> modules.getGroupsModule().joinGroupByToken(token) @@ -1570,13 +1756,25 @@ public Command joinGroupViaToken(String token) { .failure(e -> callback.onError(e)); } + /** + * Join group + * + * @param gid group's id + * @return Promise of Void + */ + @NotNull + @ObjectiveCName("joinGroupWithGid:") + public Promise joinGroup(int gid) { + return modules.getGroupsModule().joinGroup(gid); + } + /** * Request integration token for group * * @param gid group's id * @return Command for execution */ - @Nullable + @NotNull @ObjectiveCName("requestIntegrationTokenCommandWithGid:") public Command requestIntegrationToken(int gid) { return callback -> modules.getGroupsModule().requestIntegrationToken(gid) @@ -2241,6 +2439,106 @@ public void changeAnimationAutoPlayEnabled(boolean val) { modules.getSettingsModule().setAnimationAutoPlayEnabled(val); } + /** + * Is animation content auto download enabled + * + * @return is animation auto download enabled + */ + @ObjectiveCName("isAnimationAutoDownloadEnabled") + public boolean isAnimationAutoDownloadEnabled() { + return modules.getSettingsModule().isAnimationAutoDownloadEnabled(); + } + + /** + * Change animation auto download enabled + * + * @param val is auto download enabled + */ + @ObjectiveCName("changeAnimationAutoDownloadEnabled:") + public void changeAnimationAutoDownloadEnabled(boolean val) { + modules.getSettingsModule().setAnimationAutoDownloadEnabled(val); + } + + /** + * Is image content auto download enabled + * + * @return is image auto download enabled + */ + @ObjectiveCName("isImageAutoDownloadEnabled") + public boolean isImageAutoDownloadEnabled() { + return modules.getSettingsModule().isImageAutoDownloadEnabled(); + } + + /** + * Change image auto download enabled + * + * @param val is auto download enabled + */ + @ObjectiveCName("changeImageAutoDownloadEnabled:") + public void changeImageAutoDownloadEnabled(boolean val) { + modules.getSettingsModule().setImageAutoDownloadEnabled(val); + } + + /** + * Is video content auto download enabled + * + * @return is video auto download enabled + */ + @ObjectiveCName("isVideoAutoDownloadEnabled") + public boolean isVideoAutoDownloadEnabled() { + return modules.getSettingsModule().isVideoAutoDownloadEnabled(); + } + + /** + * Change video auto download enabled + * + * @param val is auto download enabled + */ + @ObjectiveCName("changeVideoAutoDownloadEnabled:") + public void changeVideoAutoDownloadEnabled(boolean val) { + modules.getSettingsModule().setVideoAutoDownloadEnabled(val); + } + + /** + * Is audio content auto download enabled + * + * @return is audio auto download enabled + */ + @ObjectiveCName("isAudioAutoDownloadEnabled") + public boolean isAudioAutoDownloadEnabled() { + return modules.getSettingsModule().isAudioAutoDownloadEnabled(); + } + + /** + * Change audio auto download enabled + * + * @param val is auto download enabled + */ + @ObjectiveCName("changeAudioAutoDownloadEnabled:") + public void changeAudioAutoDownloadEnabled(boolean val) { + modules.getSettingsModule().setAudioAutoDownloadEnabled(val); + } + + /** + * Is doc content auto download enabled + * + * @return is doc auto download enabled + */ + @ObjectiveCName("isDocAutoDownloadEnabled") + public boolean isDocAutoDownloadEnabled() { + return modules.getSettingsModule().isDocAutoDownloadEnabled(); + } + + /** + * Change doc auto download enabled + * + * @param val is auto download enabled + */ + @ObjectiveCName("changeDocAutoDownloadEnabled:") + public void changeDocAutoDownloadEnabled(boolean val) { + modules.getSettingsModule().setDocAutoDownloadEnabled(val); + } + ////////////////////////////////////// // Security diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/RawUpdatesHandler.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/RawUpdatesHandler.java new file mode 100644 index 0000000000..923487ba8b --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/RawUpdatesHandler.java @@ -0,0 +1,12 @@ +package im.actor.core; + +import im.actor.core.api.updates.UpdateRawUpdate; +import im.actor.runtime.actors.Actor; +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.promise.Promise; + +public abstract class RawUpdatesHandler { + + public abstract Promise onRawUpdate(UpdateRawUpdate update); + +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiAdminSettings.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiAdminSettings.java new file mode 100644 index 0000000000..b8ee0de80d --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiAdminSettings.java @@ -0,0 +1,97 @@ +package im.actor.core.api; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +public class ApiAdminSettings extends BserObject { + + private boolean showAdminsToMembers; + private boolean canMembersInvite; + private boolean canMembersEditGroupInfo; + private boolean canAdminsEditGroupInfo; + private boolean showJoinLeaveMessages; + + public ApiAdminSettings(boolean showAdminsToMembers, boolean canMembersInvite, boolean canMembersEditGroupInfo, boolean canAdminsEditGroupInfo, boolean showJoinLeaveMessages) { + this.showAdminsToMembers = showAdminsToMembers; + this.canMembersInvite = canMembersInvite; + this.canMembersEditGroupInfo = canMembersEditGroupInfo; + this.canAdminsEditGroupInfo = canAdminsEditGroupInfo; + this.showJoinLeaveMessages = showJoinLeaveMessages; + } + + public ApiAdminSettings() { + + } + + public boolean showAdminsToMembers() { + return this.showAdminsToMembers; + } + + public boolean canMembersInvite() { + return this.canMembersInvite; + } + + public boolean canMembersEditGroupInfo() { + return this.canMembersEditGroupInfo; + } + + public boolean canAdminsEditGroupInfo() { + return this.canAdminsEditGroupInfo; + } + + public boolean showJoinLeaveMessages() { + return this.showJoinLeaveMessages; + } + + @Override + public void parse(BserValues values) throws IOException { + this.showAdminsToMembers = values.getBool(1); + this.canMembersInvite = values.getBool(2); + this.canMembersEditGroupInfo = values.getBool(3); + this.canAdminsEditGroupInfo = values.getBool(4); + this.showJoinLeaveMessages = values.getBool(5); + if (values.hasRemaining()) { + setUnmappedObjects(values.buildRemaining()); + } + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeBool(1, this.showAdminsToMembers); + writer.writeBool(2, this.canMembersInvite); + writer.writeBool(3, this.canMembersEditGroupInfo); + writer.writeBool(4, this.canAdminsEditGroupInfo); + writer.writeBool(5, this.showJoinLeaveMessages); + if (this.getUnmappedObjects() != null) { + SparseArray unmapped = this.getUnmappedObjects(); + for (int i = 0; i < unmapped.size(); i++) { + int key = unmapped.keyAt(i); + writer.writeUnmapped(key, unmapped.get(key)); + } + } + } + + @Override + public String toString() { + String res = "struct AdminSettings{"; + res += "showAdminsToMembers=" + this.showAdminsToMembers; + res += ", canMembersInvite=" + this.canMembersInvite; + res += ", canMembersEditGroupInfo=" + this.canMembersEditGroupInfo; + res += ", canAdminsEditGroupInfo=" + this.canAdminsEditGroupInfo; + res += ", showJoinLeaveMessages=" + this.showJoinLeaveMessages; + res += "}"; + return res; + } + +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiGroup.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiGroup.java index 788c3223e9..57cbf4d365 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiGroup.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiGroup.java @@ -24,16 +24,11 @@ public class ApiGroup extends BserObject { private Boolean isMember; private Boolean isHidden; private ApiGroupType groupType; - private Boolean canSendMessage; + private Long permissions; + private Boolean isDeleted; private ApiMapValue ext; - private Boolean isAdmin; - private int creatorUid; - private List members; - private long createDate; - private String theme; - private String about; - - public ApiGroup(int id, long accessHash, @NotNull String title, @Nullable ApiAvatar avatar, @Nullable Integer membersCount, @Nullable Boolean isMember, @Nullable Boolean isHidden, @Nullable ApiGroupType groupType, @Nullable Boolean canSendMessage, @Nullable ApiMapValue ext, @Nullable Boolean isAdmin, int creatorUid, @NotNull List members, long createDate, @Nullable String theme, @Nullable String about) { + + public ApiGroup(int id, long accessHash, @NotNull String title, @Nullable ApiAvatar avatar, @Nullable Integer membersCount, @Nullable Boolean isMember, @Nullable Boolean isHidden, @Nullable ApiGroupType groupType, @Nullable Long permissions, @Nullable Boolean isDeleted, @Nullable ApiMapValue ext) { this.id = id; this.accessHash = accessHash; this.title = title; @@ -42,14 +37,9 @@ public ApiGroup(int id, long accessHash, @NotNull String title, @Nullable ApiAva this.isMember = isMember; this.isHidden = isHidden; this.groupType = groupType; - this.canSendMessage = canSendMessage; + this.permissions = permissions; + this.isDeleted = isDeleted; this.ext = ext; - this.isAdmin = isAdmin; - this.creatorUid = creatorUid; - this.members = members; - this.createDate = createDate; - this.theme = theme; - this.about = about; } public ApiGroup() { @@ -95,41 +85,18 @@ public ApiGroupType getGroupType() { } @Nullable - public Boolean canSendMessage() { - return this.canSendMessage; + public Long getPermissions() { + return this.permissions; } @Nullable - public ApiMapValue getExt() { - return this.ext; + public Boolean isDeleted() { + return this.isDeleted; } @Nullable - public Boolean isAdmin() { - return this.isAdmin; - } - - public int getCreatorUid() { - return this.creatorUid; - } - - @NotNull - public List getMembers() { - return this.members; - } - - public long getCreateDate() { - return this.createDate; - } - - @Nullable - public String getTheme() { - return this.theme; - } - - @Nullable - public String getAbout() { - return this.about; + public ApiMapValue getExt() { + return this.ext; } @Override @@ -145,18 +112,9 @@ public void parse(BserValues values) throws IOException { if (val_groupType != 0) { this.groupType = ApiGroupType.parse(val_groupType); } - this.canSendMessage = values.optBool(26); + this.permissions = values.optLong(26); + this.isDeleted = values.optBool(27); this.ext = values.optObj(22, new ApiMapValue()); - this.isAdmin = values.optBool(16); - this.creatorUid = values.getInt(8); - List _members = new ArrayList(); - for (int i = 0; i < values.getRepeatedCount(9); i ++) { - _members.add(new ApiMember()); - } - this.members = values.getRepeatedObj(9, _members); - this.createDate = values.getLong(10); - this.theme = values.optString(17); - this.about = values.optString(18); if (values.hasRemaining()) { setUnmappedObjects(values.buildRemaining()); } @@ -185,24 +143,15 @@ public void serialize(BserWriter writer) throws IOException { if (this.groupType != null) { writer.writeInt(25, this.groupType.getValue()); } - if (this.canSendMessage != null) { - writer.writeBool(26, this.canSendMessage); + if (this.permissions != null) { + writer.writeLong(26, this.permissions); + } + if (this.isDeleted != null) { + writer.writeBool(27, this.isDeleted); } if (this.ext != null) { writer.writeObject(22, this.ext); } - if (this.isAdmin != null) { - writer.writeBool(16, this.isAdmin); - } - writer.writeInt(8, this.creatorUid); - writer.writeRepeatedObj(9, this.members); - writer.writeLong(10, this.createDate); - if (this.theme != null) { - writer.writeString(17, this.theme); - } - if (this.about != null) { - writer.writeString(18, this.about); - } if (this.getUnmappedObjects() != null) { SparseArray unmapped = this.getUnmappedObjects(); for (int i = 0; i < unmapped.size(); i++) { @@ -222,11 +171,9 @@ public String toString() { res += ", isMember=" + this.isMember; res += ", isHidden=" + this.isHidden; res += ", groupType=" + this.groupType; - res += ", canSendMessage=" + this.canSendMessage; + res += ", permissions=" + this.permissions; + res += ", isDeleted=" + this.isDeleted; res += ", ext=" + this.ext; - res += ", isAdmin=" + this.isAdmin; - res += ", members=" + this.members.size(); - res += ", createDate=" + this.createDate; res += "}"; return res; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiGroupFull.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiGroupFull.java index 843e07075d..fe8aeb83de 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiGroupFull.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiGroupFull.java @@ -18,17 +18,17 @@ public class ApiGroupFull extends BserObject { private int id; private long createDate; - private int ownerUid; + private Integer ownerUid; private List members; private String theme; private String about; private ApiMapValue ext; private Boolean isAsyncMembers; - private Boolean canViewMembers; - private Boolean canInvitePeople; private Boolean isSharedHistory; + private String shortName; + private Long permissions; - public ApiGroupFull(int id, long createDate, int ownerUid, @NotNull List members, @Nullable String theme, @Nullable String about, @Nullable ApiMapValue ext, @Nullable Boolean isAsyncMembers, @Nullable Boolean canViewMembers, @Nullable Boolean canInvitePeople, @Nullable Boolean isSharedHistory) { + public ApiGroupFull(int id, long createDate, @Nullable Integer ownerUid, @NotNull List members, @Nullable String theme, @Nullable String about, @Nullable ApiMapValue ext, @Nullable Boolean isAsyncMembers, @Nullable Boolean isSharedHistory, @Nullable String shortName, @Nullable Long permissions) { this.id = id; this.createDate = createDate; this.ownerUid = ownerUid; @@ -37,9 +37,9 @@ public ApiGroupFull(int id, long createDate, int ownerUid, @NotNull List _members = new ArrayList(); for (int i = 0; i < values.getRepeatedCount(12); i ++) { _members.add(new ApiMember()); @@ -112,16 +113,21 @@ public void parse(BserValues values) throws IOException { this.about = values.optString(3); this.ext = values.optObj(7, new ApiMapValue()); this.isAsyncMembers = values.optBool(11); - this.canViewMembers = values.optBool(8); - this.canInvitePeople = values.optBool(9); this.isSharedHistory = values.optBool(10); + this.shortName = values.optString(14); + this.permissions = values.optLong(27); + if (values.hasRemaining()) { + setUnmappedObjects(values.buildRemaining()); + } } @Override public void serialize(BserWriter writer) throws IOException { writer.writeInt(1, this.id); writer.writeLong(6, this.createDate); - writer.writeInt(5, this.ownerUid); + if (this.ownerUid != null) { + writer.writeInt(5, this.ownerUid); + } writer.writeRepeatedObj(12, this.members); if (this.theme != null) { writer.writeString(2, this.theme); @@ -135,15 +141,22 @@ public void serialize(BserWriter writer) throws IOException { if (this.isAsyncMembers != null) { writer.writeBool(11, this.isAsyncMembers); } - if (this.canViewMembers != null) { - writer.writeBool(8, this.canViewMembers); - } - if (this.canInvitePeople != null) { - writer.writeBool(9, this.canInvitePeople); - } if (this.isSharedHistory != null) { writer.writeBool(10, this.isSharedHistory); } + if (this.shortName != null) { + writer.writeString(14, this.shortName); + } + if (this.permissions != null) { + writer.writeLong(27, this.permissions); + } + if (this.getUnmappedObjects() != null) { + SparseArray unmapped = this.getUnmappedObjects(); + for (int i = 0; i < unmapped.size(); i++) { + int key = unmapped.keyAt(i); + writer.writeUnmapped(key, unmapped.get(key)); + } + } } @Override @@ -156,9 +169,9 @@ public String toString() { res += ", theme=" + this.theme; res += ", about=" + this.about; res += ", isAsyncMembers=" + this.isAsyncMembers; - res += ", canViewMembers=" + this.canViewMembers; - res += ", canInvitePeople=" + this.canInvitePeople; res += ", isSharedHistory=" + this.isSharedHistory; + res += ", shortName=" + this.shortName; + res += ", permissions=" + this.permissions; res += "}"; return res; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiGroupFullPermissions.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiGroupFullPermissions.java new file mode 100644 index 0000000000..502c59432c --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiGroupFullPermissions.java @@ -0,0 +1,51 @@ +package im.actor.core.api; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import java.io.IOException; + +public enum ApiGroupFullPermissions { + + EDIT_INFO(1), + VIEW_MEMBERS(2), + INVITE_MEMBERS(3), + INVITE_VIA_LINK(4), + CALL(5), + EDIT_ADMIN_SETTINGS(6), + VIEW_ADMINS(7), + EDIT_ADMINS(8), + KICK_INVITED(9), + KICK_ANYONE(10), + EDIT_FOREIGN(11), + DELETE_FOREIGN(12), + UNSUPPORTED_VALUE(-1); + + private int value; + + ApiGroupFullPermissions(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static ApiGroupFullPermissions parse(int value) throws IOException { + switch(value) { + case 1: return ApiGroupFullPermissions.EDIT_INFO; + case 2: return ApiGroupFullPermissions.VIEW_MEMBERS; + case 3: return ApiGroupFullPermissions.INVITE_MEMBERS; + case 4: return ApiGroupFullPermissions.INVITE_VIA_LINK; + case 5: return ApiGroupFullPermissions.CALL; + case 6: return ApiGroupFullPermissions.EDIT_ADMIN_SETTINGS; + case 7: return ApiGroupFullPermissions.VIEW_ADMINS; + case 8: return ApiGroupFullPermissions.EDIT_ADMINS; + case 9: return ApiGroupFullPermissions.KICK_INVITED; + case 10: return ApiGroupFullPermissions.KICK_ANYONE; + case 11: return ApiGroupFullPermissions.EDIT_FOREIGN; + case 12: return ApiGroupFullPermissions.DELETE_FOREIGN; + default: return ApiGroupFullPermissions.UNSUPPORTED_VALUE; + } + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiGroupPermissions.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiGroupPermissions.java new file mode 100644 index 0000000000..cc67d1d30e --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiGroupPermissions.java @@ -0,0 +1,39 @@ +package im.actor.core.api; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import java.io.IOException; + +public enum ApiGroupPermissions { + + SEND_MESSAGE(1), + CLEAR(2), + LEAVE(3), + DELETE(4), + JOIN(5), + VIEW_INFO(6), + UNSUPPORTED_VALUE(-1); + + private int value; + + ApiGroupPermissions(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static ApiGroupPermissions parse(int value) throws IOException { + switch(value) { + case 1: return ApiGroupPermissions.SEND_MESSAGE; + case 2: return ApiGroupPermissions.CLEAR; + case 3: return ApiGroupPermissions.LEAVE; + case 4: return ApiGroupPermissions.DELETE; + case 5: return ApiGroupPermissions.JOIN; + case 6: return ApiGroupPermissions.VIEW_INFO; + default: return ApiGroupPermissions.UNSUPPORTED_VALUE; + } + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiKeyGroupHolder.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiKeyGroupHolder.java new file mode 100644 index 0000000000..e21577a32c --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiKeyGroupHolder.java @@ -0,0 +1,64 @@ +package im.actor.core.api; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +public class ApiKeyGroupHolder extends BserObject { + + private int uid; + private ApiEncryptionKeyGroup keyGroup; + + public ApiKeyGroupHolder(int uid, @NotNull ApiEncryptionKeyGroup keyGroup) { + this.uid = uid; + this.keyGroup = keyGroup; + } + + public ApiKeyGroupHolder() { + + } + + public int getUid() { + return this.uid; + } + + @NotNull + public ApiEncryptionKeyGroup getKeyGroup() { + return this.keyGroup; + } + + @Override + public void parse(BserValues values) throws IOException { + this.uid = values.getInt(1); + this.keyGroup = values.getObj(2, new ApiEncryptionKeyGroup()); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeInt(1, this.uid); + if (this.keyGroup == null) { + throw new IOException(); + } + writer.writeObject(2, this.keyGroup); + } + + @Override + public String toString() { + String res = "struct KeyGroupHolder{"; + res += "uid=" + this.uid; + res += ", keyGroup=" + this.keyGroup; + res += "}"; + return res; + } + +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiKeyGroupId.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiKeyGroupId.java index 05d1d404a5..44b13d3f3b 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiKeyGroupId.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiKeyGroupId.java @@ -52,6 +52,7 @@ public void serialize(BserWriter writer) throws IOException { public String toString() { String res = "struct KeyGroupId{"; res += "uid=" + this.uid; + res += ", keyGroupId=" + this.keyGroupId; res += "}"; return res; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiPeerSearchResult.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiPeerSearchResult.java index 2db3b2d8fb..a12c4a85a6 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiPeerSearchResult.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/ApiPeerSearchResult.java @@ -17,23 +17,11 @@ public class ApiPeerSearchResult extends BserObject { private ApiPeer peer; - private String title; - private String description; - private Integer membersCount; - private Long dateCreated; - private Integer creator; - private Boolean isPublic; - private Boolean isJoined; + private String optMatchString; - public ApiPeerSearchResult(@NotNull ApiPeer peer, @NotNull String title, @Nullable String description, @Nullable Integer membersCount, @Nullable Long dateCreated, @Nullable Integer creator, @Nullable Boolean isPublic, @Nullable Boolean isJoined) { + public ApiPeerSearchResult(@NotNull ApiPeer peer, @Nullable String optMatchString) { this.peer = peer; - this.title = title; - this.description = description; - this.membersCount = membersCount; - this.dateCreated = dateCreated; - this.creator = creator; - this.isPublic = isPublic; - this.isJoined = isJoined; + this.optMatchString = optMatchString; } public ApiPeerSearchResult() { @@ -45,51 +33,15 @@ public ApiPeer getPeer() { return this.peer; } - @NotNull - public String getTitle() { - return this.title; - } - @Nullable - public String getDescription() { - return this.description; - } - - @Nullable - public Integer getMembersCount() { - return this.membersCount; - } - - @Nullable - public Long getDateCreated() { - return this.dateCreated; - } - - @Nullable - public Integer getCreator() { - return this.creator; - } - - @Nullable - public Boolean isPublic() { - return this.isPublic; - } - - @Nullable - public Boolean isJoined() { - return this.isJoined; + public String getOptMatchString() { + return this.optMatchString; } @Override public void parse(BserValues values) throws IOException { this.peer = values.getObj(1, new ApiPeer()); - this.title = values.getString(2); - this.description = values.optString(3); - this.membersCount = values.optInt(4); - this.dateCreated = values.optLong(5); - this.creator = values.optInt(6); - this.isPublic = values.optBool(7); - this.isJoined = values.optBool(8); + this.optMatchString = values.optString(3); } @Override @@ -98,27 +50,8 @@ public void serialize(BserWriter writer) throws IOException { throw new IOException(); } writer.writeObject(1, this.peer); - if (this.title == null) { - throw new IOException(); - } - writer.writeString(2, this.title); - if (this.description != null) { - writer.writeString(3, this.description); - } - if (this.membersCount != null) { - writer.writeInt(4, this.membersCount); - } - if (this.dateCreated != null) { - writer.writeLong(5, this.dateCreated); - } - if (this.creator != null) { - writer.writeInt(6, this.creator); - } - if (this.isPublic != null) { - writer.writeBool(7, this.isPublic); - } - if (this.isJoined != null) { - writer.writeBool(8, this.isJoined); + if (this.optMatchString != null) { + writer.writeString(3, this.optMatchString); } } @@ -126,12 +59,7 @@ public void serialize(BserWriter writer) throws IOException { public String toString() { String res = "struct PeerSearchResult{"; res += "peer=" + this.peer; - res += ", title=" + this.title; - res += ", description=" + this.description; - res += ", membersCount=" + this.membersCount; - res += ", dateCreated=" + this.dateCreated; - res += ", creator=" + this.creator; - res += ", isPublic=" + this.isPublic; + res += ", optMatchString=" + this.optMatchString; res += "}"; return res; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/parser/RpcParser.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/parser/RpcParser.java index ce1b5d0096..e628db24b1 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/parser/RpcParser.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/parser/RpcParser.java @@ -82,13 +82,21 @@ public RpcScope read(int type, byte[] payload) throws IOException { case 85: return RequestEditGroupTitle.fromBytes(payload); case 86: return RequestEditGroupAvatar.fromBytes(payload); case 101: return RequestRemoveGroupAvatar.fromBytes(payload); + case 2793: return RequestEditGroupShortName.fromBytes(payload); case 211: return RequestEditGroupTopic.fromBytes(payload); case 213: return RequestEditGroupAbout.fromBytes(payload); case 69: return RequestInviteUser.fromBytes(payload); case 70: return RequestLeaveGroup.fromBytes(payload); + case 2721: return RequestLeaveAndDelete.fromBytes(payload); case 71: return RequestKickUser.fromBytes(payload); + case 2722: return RequestJoinGroupByPeer.fromBytes(payload); case 2784: return RequestMakeUserAdmin.fromBytes(payload); + case 2791: return RequestDismissUserAdmin.fromBytes(payload); case 2789: return RequestTransferOwnership.fromBytes(payload); + case 2790: return RequestLoadAdminSettings.fromBytes(payload); + case 2792: return RequestSaveAdminSettings.fromBytes(payload); + case 2795: return RequestDeleteGroup.fromBytes(payload); + case 2796: return RequestShareHistory.fromBytes(payload); case 177: return RequestGetGroupInviteUrl.fromBytes(payload); case 179: return RequestRevokeInviteUrl.fromBytes(payload); case 180: return RequestJoinGroup.fromBytes(payload); @@ -197,6 +205,7 @@ public RpcScope read(int type, byte[] payload) throws IOException { case 2787: return ResponseLoadMembers.fromBytes(payload); case 216: return ResponseCreateGroup.fromBytes(payload); case 115: return ResponseEditGroupAvatar.fromBytes(payload); + case 2794: return ResponseLoadAdminSettings.fromBytes(payload); case 178: return ResponseInviteUrl.fromBytes(payload); case 181: return ResponseJoinGroup.fromBytes(payload); case 66: return ResponseCreateGroupObsolete.fromBytes(payload); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/parser/UpdatesParser.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/parser/UpdatesParser.java index 4a3ce5315c..5bd8ea0008 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/parser/UpdatesParser.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/parser/UpdatesParser.java @@ -47,6 +47,7 @@ public Update read(int type, byte[] payload) throws IOException { case 47: return UpdateChatClear.fromBytes(payload); case 48: return UpdateChatDelete.fromBytes(payload); case 94: return UpdateChatArchive.fromBytes(payload); + case 2690: return UpdateChatDropCache.fromBytes(payload); case 1: return UpdateChatGroupsChanged.fromBytes(payload); case 222: return UpdateReactionsUpdate.fromBytes(payload); case 2609: return UpdateGroupTitleChanged.fromBytes(payload); @@ -55,11 +56,12 @@ public Update read(int type, byte[] payload) throws IOException { case 2617: return UpdateGroupAboutChanged.fromBytes(payload); case 2613: return UpdateGroupExtChanged.fromBytes(payload); case 2618: return UpdateGroupFullExtChanged.fromBytes(payload); + case 2628: return UpdateGroupShortNameChanged.fromBytes(payload); case 2619: return UpdateGroupOwnerChanged.fromBytes(payload); case 2620: return UpdateGroupHistoryShared.fromBytes(payload); - case 2624: return UpdateGroupCanSendMessagesChanged.fromBytes(payload); - case 2625: return UpdateGroupCanViewMembersChanged.fromBytes(payload); - case 2626: return UpdateGroupCanInviteMembersChanged.fromBytes(payload); + case 2658: return UpdateGroupDeleted.fromBytes(payload); + case 2663: return UpdateGroupPermissionsChanged.fromBytes(payload); + case 2664: return UpdateGroupFullPermissionsChanged.fromBytes(payload); case 2612: return UpdateGroupMemberChanged.fromBytes(payload); case 2615: return UpdateGroupMembersBecameAsync.fromBytes(payload); case 2614: return UpdateGroupMembersUpdated.fromBytes(payload); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestDeleteGroup.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestDeleteGroup.java new file mode 100644 index 0000000000..2e1be595fd --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestDeleteGroup.java @@ -0,0 +1,65 @@ +package im.actor.core.api.rpc; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; +import im.actor.core.api.*; + +public class RequestDeleteGroup extends Request { + + public static final int HEADER = 0xaeb; + public static RequestDeleteGroup fromBytes(byte[] data) throws IOException { + return Bser.parse(new RequestDeleteGroup(), data); + } + + private ApiGroupOutPeer groupPeer; + + public RequestDeleteGroup(@NotNull ApiGroupOutPeer groupPeer) { + this.groupPeer = groupPeer; + } + + public RequestDeleteGroup() { + + } + + @NotNull + public ApiGroupOutPeer getGroupPeer() { + return this.groupPeer; + } + + @Override + public void parse(BserValues values) throws IOException { + this.groupPeer = values.getObj(1, new ApiGroupOutPeer()); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + if (this.groupPeer == null) { + throw new IOException(); + } + writer.writeObject(1, this.groupPeer); + } + + @Override + public String toString() { + String res = "rpc DeleteGroup{"; + res += "groupPeer=" + this.groupPeer; + res += "}"; + return res; + } + + @Override + public int getHeaderKey() { + return HEADER; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestDismissUserAdmin.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestDismissUserAdmin.java new file mode 100644 index 0000000000..30833991c6 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestDismissUserAdmin.java @@ -0,0 +1,78 @@ +package im.actor.core.api.rpc; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; +import im.actor.core.api.*; + +public class RequestDismissUserAdmin extends Request { + + public static final int HEADER = 0xae7; + public static RequestDismissUserAdmin fromBytes(byte[] data) throws IOException { + return Bser.parse(new RequestDismissUserAdmin(), data); + } + + private ApiGroupOutPeer groupPeer; + private ApiUserOutPeer userPeer; + + public RequestDismissUserAdmin(@NotNull ApiGroupOutPeer groupPeer, @NotNull ApiUserOutPeer userPeer) { + this.groupPeer = groupPeer; + this.userPeer = userPeer; + } + + public RequestDismissUserAdmin() { + + } + + @NotNull + public ApiGroupOutPeer getGroupPeer() { + return this.groupPeer; + } + + @NotNull + public ApiUserOutPeer getUserPeer() { + return this.userPeer; + } + + @Override + public void parse(BserValues values) throws IOException { + this.groupPeer = values.getObj(1, new ApiGroupOutPeer()); + this.userPeer = values.getObj(2, new ApiUserOutPeer()); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + if (this.groupPeer == null) { + throw new IOException(); + } + writer.writeObject(1, this.groupPeer); + if (this.userPeer == null) { + throw new IOException(); + } + writer.writeObject(2, this.userPeer); + } + + @Override + public String toString() { + String res = "rpc DismissUserAdmin{"; + res += "groupPeer=" + this.groupPeer; + res += ", userPeer=" + this.userPeer; + res += "}"; + return res; + } + + @Override + public int getHeaderKey() { + return HEADER; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestEditGroupShortName.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestEditGroupShortName.java new file mode 100644 index 0000000000..fc5d139256 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestEditGroupShortName.java @@ -0,0 +1,77 @@ +package im.actor.core.api.rpc; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; +import im.actor.core.api.*; + +public class RequestEditGroupShortName extends Request { + + public static final int HEADER = 0xae9; + public static RequestEditGroupShortName fromBytes(byte[] data) throws IOException { + return Bser.parse(new RequestEditGroupShortName(), data); + } + + private ApiGroupOutPeer groupPeer; + private String shortName; + + public RequestEditGroupShortName(@NotNull ApiGroupOutPeer groupPeer, @Nullable String shortName) { + this.groupPeer = groupPeer; + this.shortName = shortName; + } + + public RequestEditGroupShortName() { + + } + + @NotNull + public ApiGroupOutPeer getGroupPeer() { + return this.groupPeer; + } + + @Nullable + public String getShortName() { + return this.shortName; + } + + @Override + public void parse(BserValues values) throws IOException { + this.groupPeer = values.getObj(1, new ApiGroupOutPeer()); + this.shortName = values.optString(2); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + if (this.groupPeer == null) { + throw new IOException(); + } + writer.writeObject(1, this.groupPeer); + if (this.shortName != null) { + writer.writeString(2, this.shortName); + } + } + + @Override + public String toString() { + String res = "rpc EditGroupShortName{"; + res += "groupPeer=" + this.groupPeer; + res += ", shortName=" + this.shortName; + res += "}"; + return res; + } + + @Override + public int getHeaderKey() { + return HEADER; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestJoinGroupByPeer.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestJoinGroupByPeer.java new file mode 100644 index 0000000000..261437e7b3 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestJoinGroupByPeer.java @@ -0,0 +1,65 @@ +package im.actor.core.api.rpc; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; +import im.actor.core.api.*; + +public class RequestJoinGroupByPeer extends Request { + + public static final int HEADER = 0xaa2; + public static RequestJoinGroupByPeer fromBytes(byte[] data) throws IOException { + return Bser.parse(new RequestJoinGroupByPeer(), data); + } + + private ApiGroupOutPeer groupPeer; + + public RequestJoinGroupByPeer(@NotNull ApiGroupOutPeer groupPeer) { + this.groupPeer = groupPeer; + } + + public RequestJoinGroupByPeer() { + + } + + @NotNull + public ApiGroupOutPeer getGroupPeer() { + return this.groupPeer; + } + + @Override + public void parse(BserValues values) throws IOException { + this.groupPeer = values.getObj(1, new ApiGroupOutPeer()); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + if (this.groupPeer == null) { + throw new IOException(); + } + writer.writeObject(1, this.groupPeer); + } + + @Override + public String toString() { + String res = "rpc JoinGroupByPeer{"; + res += "groupPeer=" + this.groupPeer; + res += "}"; + return res; + } + + @Override + public int getHeaderKey() { + return HEADER; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestLeaveAndDelete.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestLeaveAndDelete.java new file mode 100644 index 0000000000..810dd0526f --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestLeaveAndDelete.java @@ -0,0 +1,65 @@ +package im.actor.core.api.rpc; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; +import im.actor.core.api.*; + +public class RequestLeaveAndDelete extends Request { + + public static final int HEADER = 0xaa1; + public static RequestLeaveAndDelete fromBytes(byte[] data) throws IOException { + return Bser.parse(new RequestLeaveAndDelete(), data); + } + + private ApiGroupOutPeer groupPeer; + + public RequestLeaveAndDelete(@NotNull ApiGroupOutPeer groupPeer) { + this.groupPeer = groupPeer; + } + + public RequestLeaveAndDelete() { + + } + + @NotNull + public ApiGroupOutPeer getGroupPeer() { + return this.groupPeer; + } + + @Override + public void parse(BserValues values) throws IOException { + this.groupPeer = values.getObj(1, new ApiGroupOutPeer()); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + if (this.groupPeer == null) { + throw new IOException(); + } + writer.writeObject(1, this.groupPeer); + } + + @Override + public String toString() { + String res = "rpc LeaveAndDelete{"; + res += "groupPeer=" + this.groupPeer; + res += "}"; + return res; + } + + @Override + public int getHeaderKey() { + return HEADER; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestLoadAdminSettings.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestLoadAdminSettings.java new file mode 100644 index 0000000000..3cf4818a73 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestLoadAdminSettings.java @@ -0,0 +1,65 @@ +package im.actor.core.api.rpc; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; +import im.actor.core.api.*; + +public class RequestLoadAdminSettings extends Request { + + public static final int HEADER = 0xae6; + public static RequestLoadAdminSettings fromBytes(byte[] data) throws IOException { + return Bser.parse(new RequestLoadAdminSettings(), data); + } + + private ApiGroupOutPeer groupPeer; + + public RequestLoadAdminSettings(@NotNull ApiGroupOutPeer groupPeer) { + this.groupPeer = groupPeer; + } + + public RequestLoadAdminSettings() { + + } + + @NotNull + public ApiGroupOutPeer getGroupPeer() { + return this.groupPeer; + } + + @Override + public void parse(BserValues values) throws IOException { + this.groupPeer = values.getObj(1, new ApiGroupOutPeer()); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + if (this.groupPeer == null) { + throw new IOException(); + } + writer.writeObject(1, this.groupPeer); + } + + @Override + public String toString() { + String res = "rpc LoadAdminSettings{"; + res += "groupPeer=" + this.groupPeer; + res += "}"; + return res; + } + + @Override + public int getHeaderKey() { + return HEADER; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestSaveAdminSettings.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestSaveAdminSettings.java new file mode 100644 index 0000000000..0639937ed5 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestSaveAdminSettings.java @@ -0,0 +1,78 @@ +package im.actor.core.api.rpc; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; +import im.actor.core.api.*; + +public class RequestSaveAdminSettings extends Request { + + public static final int HEADER = 0xae8; + public static RequestSaveAdminSettings fromBytes(byte[] data) throws IOException { + return Bser.parse(new RequestSaveAdminSettings(), data); + } + + private ApiGroupOutPeer groupPeer; + private ApiAdminSettings settings; + + public RequestSaveAdminSettings(@NotNull ApiGroupOutPeer groupPeer, @NotNull ApiAdminSettings settings) { + this.groupPeer = groupPeer; + this.settings = settings; + } + + public RequestSaveAdminSettings() { + + } + + @NotNull + public ApiGroupOutPeer getGroupPeer() { + return this.groupPeer; + } + + @NotNull + public ApiAdminSettings getSettings() { + return this.settings; + } + + @Override + public void parse(BserValues values) throws IOException { + this.groupPeer = values.getObj(1, new ApiGroupOutPeer()); + this.settings = values.getObj(2, new ApiAdminSettings()); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + if (this.groupPeer == null) { + throw new IOException(); + } + writer.writeObject(1, this.groupPeer); + if (this.settings == null) { + throw new IOException(); + } + writer.writeObject(2, this.settings); + } + + @Override + public String toString() { + String res = "rpc SaveAdminSettings{"; + res += "groupPeer=" + this.groupPeer; + res += ", settings=" + this.settings; + res += "}"; + return res; + } + + @Override + public int getHeaderKey() { + return HEADER; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestShareHistory.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestShareHistory.java new file mode 100644 index 0000000000..1ea3fc3fa7 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestShareHistory.java @@ -0,0 +1,65 @@ +package im.actor.core.api.rpc; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; +import im.actor.core.api.*; + +public class RequestShareHistory extends Request { + + public static final int HEADER = 0xaec; + public static RequestShareHistory fromBytes(byte[] data) throws IOException { + return Bser.parse(new RequestShareHistory(), data); + } + + private ApiGroupOutPeer groupPeer; + + public RequestShareHistory(@NotNull ApiGroupOutPeer groupPeer) { + this.groupPeer = groupPeer; + } + + public RequestShareHistory() { + + } + + @NotNull + public ApiGroupOutPeer getGroupPeer() { + return this.groupPeer; + } + + @Override + public void parse(BserValues values) throws IOException { + this.groupPeer = values.getObj(1, new ApiGroupOutPeer()); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + if (this.groupPeer == null) { + throw new IOException(); + } + writer.writeObject(1, this.groupPeer); + } + + @Override + public String toString() { + String res = "rpc ShareHistory{"; + res += "groupPeer=" + this.groupPeer; + res += "}"; + return res; + } + + @Override + public int getHeaderKey() { + return HEADER; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestTransferOwnership.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestTransferOwnership.java index 5583c9b4fe..7b0ab441bb 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestTransferOwnership.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestTransferOwnership.java @@ -23,9 +23,9 @@ public static RequestTransferOwnership fromBytes(byte[] data) throws IOException } private ApiGroupOutPeer groupPeer; - private int newOwner; + private ApiUserOutPeer newOwner; - public RequestTransferOwnership(@NotNull ApiGroupOutPeer groupPeer, int newOwner) { + public RequestTransferOwnership(@NotNull ApiGroupOutPeer groupPeer, @NotNull ApiUserOutPeer newOwner) { this.groupPeer = groupPeer; this.newOwner = newOwner; } @@ -39,14 +39,15 @@ public ApiGroupOutPeer getGroupPeer() { return this.groupPeer; } - public int getNewOwner() { + @NotNull + public ApiUserOutPeer getNewOwner() { return this.newOwner; } @Override public void parse(BserValues values) throws IOException { this.groupPeer = values.getObj(1, new ApiGroupOutPeer()); - this.newOwner = values.getInt(2); + this.newOwner = values.getObj(2, new ApiUserOutPeer()); } @Override @@ -55,7 +56,10 @@ public void serialize(BserWriter writer) throws IOException { throw new IOException(); } writer.writeObject(1, this.groupPeer); - writer.writeInt(2, this.newOwner); + if (this.newOwner == null) { + throw new IOException(); + } + writer.writeObject(2, this.newOwner); } @Override diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseCreateGroup.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseCreateGroup.java index d91f7aeeb8..ab181a6f7c 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseCreateGroup.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseCreateGroup.java @@ -24,13 +24,15 @@ public static ResponseCreateGroup fromBytes(byte[] data) throws IOException { private int seq; private byte[] state; + private long date; private ApiGroup group; private List users; private List userPeers; - public ResponseCreateGroup(int seq, @NotNull byte[] state, @NotNull ApiGroup group, @NotNull List users, @NotNull List userPeers) { + public ResponseCreateGroup(int seq, @NotNull byte[] state, long date, @NotNull ApiGroup group, @NotNull List users, @NotNull List userPeers) { this.seq = seq; this.state = state; + this.date = date; this.group = group; this.users = users; this.userPeers = userPeers; @@ -49,6 +51,10 @@ public byte[] getState() { return this.state; } + public long getDate() { + return this.date; + } + @NotNull public ApiGroup getGroup() { return this.group; @@ -68,6 +74,7 @@ public List getUserPeers() { public void parse(BserValues values) throws IOException { this.seq = values.getInt(1); this.state = values.getBytes(2); + this.date = values.getLong(6); this.group = values.getObj(3, new ApiGroup()); List _users = new ArrayList(); for (int i = 0; i < values.getRepeatedCount(4); i ++) { @@ -88,6 +95,7 @@ public void serialize(BserWriter writer) throws IOException { throw new IOException(); } writer.writeBytes(2, this.state); + writer.writeLong(6, this.date); if (this.group == null) { throw new IOException(); } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseEnterGroup.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseEnterGroup.java deleted file mode 100644 index c5278e80dc..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseEnterGroup.java +++ /dev/null @@ -1,113 +0,0 @@ -package im.actor.core.api.rpc; -/* - * Generated by the Actor API Scheme generator. DO NOT EDIT! - */ - -import im.actor.runtime.bser.*; -import im.actor.runtime.collections.*; -import static im.actor.runtime.bser.Utils.*; -import im.actor.core.network.parser.*; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.NotNull; -import com.google.j2objc.annotations.ObjectiveCName; -import java.io.IOException; -import java.util.List; -import java.util.ArrayList; -import im.actor.core.api.*; - -public class ResponseEnterGroup extends Response { - - public static final int HEADER = 0xc8; - public static ResponseEnterGroup fromBytes(byte[] data) throws IOException { - return Bser.parse(new ResponseEnterGroup(), data); - } - - private ApiGroup group; - private List users; - private long rid; - private int seq; - private byte[] state; - private long date; - - public ResponseEnterGroup(@NotNull ApiGroup group, @NotNull List users, long rid, int seq, @NotNull byte[] state, long date) { - this.group = group; - this.users = users; - this.rid = rid; - this.seq = seq; - this.state = state; - this.date = date; - } - - public ResponseEnterGroup() { - - } - - @NotNull - public ApiGroup getGroup() { - return this.group; - } - - @NotNull - public List getUsers() { - return this.users; - } - - public long getRid() { - return this.rid; - } - - public int getSeq() { - return this.seq; - } - - @NotNull - public byte[] getState() { - return this.state; - } - - public long getDate() { - return this.date; - } - - @Override - public void parse(BserValues values) throws IOException { - this.group = values.getObj(1, new ApiGroup()); - List _users = new ArrayList(); - for (int i = 0; i < values.getRepeatedCount(2); i ++) { - _users.add(new ApiUser()); - } - this.users = values.getRepeatedObj(2, _users); - this.rid = values.getLong(3); - this.seq = values.getInt(4); - this.state = values.getBytes(5); - this.date = values.getLong(6); - } - - @Override - public void serialize(BserWriter writer) throws IOException { - if (this.group == null) { - throw new IOException(); - } - writer.writeObject(1, this.group); - writer.writeRepeatedObj(2, this.users); - writer.writeLong(3, this.rid); - writer.writeInt(4, this.seq); - if (this.state == null) { - throw new IOException(); - } - writer.writeBytes(5, this.state); - writer.writeLong(6, this.date); - } - - @Override - public String toString() { - String res = "tuple EnterGroup{"; - res += "}"; - return res; - } - - @Override - public int getHeaderKey() { - return HEADER; - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseLoadAdminSettings.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseLoadAdminSettings.java new file mode 100644 index 0000000000..3bb736dc6c --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseLoadAdminSettings.java @@ -0,0 +1,64 @@ +package im.actor.core.api.rpc; +/* + * Generated by the Actor API Scheme generator. DO NOT EDIT! + */ + +import im.actor.runtime.bser.*; +import im.actor.runtime.collections.*; +import static im.actor.runtime.bser.Utils.*; +import im.actor.core.network.parser.*; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import com.google.j2objc.annotations.ObjectiveCName; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; +import im.actor.core.api.*; + +public class ResponseLoadAdminSettings extends Response { + + public static final int HEADER = 0xaea; + public static ResponseLoadAdminSettings fromBytes(byte[] data) throws IOException { + return Bser.parse(new ResponseLoadAdminSettings(), data); + } + + private ApiAdminSettings settings; + + public ResponseLoadAdminSettings(@NotNull ApiAdminSettings settings) { + this.settings = settings; + } + + public ResponseLoadAdminSettings() { + + } + + @NotNull + public ApiAdminSettings getSettings() { + return this.settings; + } + + @Override + public void parse(BserValues values) throws IOException { + this.settings = values.getObj(1, new ApiAdminSettings()); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + if (this.settings == null) { + throw new IOException(); + } + writer.writeObject(1, this.settings); + } + + @Override + public String toString() { + String res = "tuple LoadAdminSettings{"; + res += "}"; + return res; + } + + @Override + public int getHeaderKey() { + return HEADER; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseLoadMembers.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseLoadMembers.java index 3410fd4287..a0936c222a 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseLoadMembers.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseLoadMembers.java @@ -22,11 +22,13 @@ public static ResponseLoadMembers fromBytes(byte[] data) throws IOException { return Bser.parse(new ResponseLoadMembers(), data); } - private List members; + private List members; + private List users; private byte[] next; - public ResponseLoadMembers(@NotNull List members, @Nullable byte[] next) { + public ResponseLoadMembers(@NotNull List members, @NotNull List users, @Nullable byte[] next) { this.members = members; + this.users = users; this.next = next; } @@ -35,10 +37,15 @@ public ResponseLoadMembers() { } @NotNull - public List getMembers() { + public List getMembers() { return this.members; } + @NotNull + public List getUsers() { + return this.users; + } + @Nullable public byte[] getNext() { return this.next; @@ -46,17 +53,23 @@ public byte[] getNext() { @Override public void parse(BserValues values) throws IOException { - List _members = new ArrayList(); + List _members = new ArrayList(); + for (int i = 0; i < values.getRepeatedCount(3); i ++) { + _members.add(new ApiMember()); + } + this.members = values.getRepeatedObj(3, _members); + List _users = new ArrayList(); for (int i = 0; i < values.getRepeatedCount(1); i ++) { - _members.add(new ApiUserOutPeer()); + _users.add(new ApiUserOutPeer()); } - this.members = values.getRepeatedObj(1, _members); + this.users = values.getRepeatedObj(1, _users); this.next = values.optBytes(2); } @Override public void serialize(BserWriter writer) throws IOException { - writer.writeRepeatedObj(1, this.members); + writer.writeRepeatedObj(3, this.members); + writer.writeRepeatedObj(1, this.users); if (this.next != null) { writer.writeBytes(2, this.next); } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseMakeUserAdmin.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseMakeUserAdmin.java deleted file mode 100644 index f1d275bc68..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseMakeUserAdmin.java +++ /dev/null @@ -1,85 +0,0 @@ -package im.actor.core.api.rpc; -/* - * Generated by the Actor API Scheme generator. DO NOT EDIT! - */ - -import im.actor.runtime.bser.*; -import im.actor.runtime.collections.*; -import static im.actor.runtime.bser.Utils.*; -import im.actor.core.network.parser.*; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.NotNull; -import com.google.j2objc.annotations.ObjectiveCName; -import java.io.IOException; -import java.util.List; -import java.util.ArrayList; -import im.actor.core.api.*; - -public class ResponseMakeUserAdmin extends Response { - - public static final int HEADER = 0xd7; - public static ResponseMakeUserAdmin fromBytes(byte[] data) throws IOException { - return Bser.parse(new ResponseMakeUserAdmin(), data); - } - - private List members; - private int seq; - private byte[] state; - - public ResponseMakeUserAdmin(@NotNull List members, int seq, @NotNull byte[] state) { - this.members = members; - this.seq = seq; - this.state = state; - } - - public ResponseMakeUserAdmin() { - - } - - @NotNull - public List getMembers() { - return this.members; - } - - public int getSeq() { - return this.seq; - } - - @NotNull - public byte[] getState() { - return this.state; - } - - @Override - public void parse(BserValues values) throws IOException { - List _members = new ArrayList(); - for (int i = 0; i < values.getRepeatedCount(1); i ++) { - _members.add(new ApiMember()); - } - this.members = values.getRepeatedObj(1, _members); - this.seq = values.getInt(2); - this.state = values.getBytes(3); - } - - @Override - public void serialize(BserWriter writer) throws IOException { - writer.writeRepeatedObj(1, this.members); - writer.writeInt(2, this.seq); - if (this.state == null) { - throw new IOException(); - } - writer.writeBytes(3, this.state); - } - - @Override - public String toString() { - String res = "tuple MakeUserAdmin{"; - res += "}"; - return res; - } - - @Override - public int getHeaderKey() { - return HEADER; - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseSendEncryptedPackage.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseSendEncryptedPackage.java index 2aabe091db..a80d449c85 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseSendEncryptedPackage.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/ResponseSendEncryptedPackage.java @@ -22,15 +22,11 @@ public static ResponseSendEncryptedPackage fromBytes(byte[] data) throws IOExcep return Bser.parse(new ResponseSendEncryptedPackage(), data); } - private Integer seq; - private byte[] state; private Long date; private List obsoleteKeyGroups; - private List missedKeyGroups; + private List missedKeyGroups; - public ResponseSendEncryptedPackage(@Nullable Integer seq, @Nullable byte[] state, @Nullable Long date, @NotNull List obsoleteKeyGroups, @NotNull List missedKeyGroups) { - this.seq = seq; - this.state = state; + public ResponseSendEncryptedPackage(@Nullable Long date, @NotNull List obsoleteKeyGroups, @NotNull List missedKeyGroups) { this.date = date; this.obsoleteKeyGroups = obsoleteKeyGroups; this.missedKeyGroups = missedKeyGroups; @@ -40,16 +36,6 @@ public ResponseSendEncryptedPackage() { } - @Nullable - public Integer getSeq() { - return this.seq; - } - - @Nullable - public byte[] getState() { - return this.state; - } - @Nullable public Long getDate() { return this.date; @@ -61,40 +47,32 @@ public List getObsoleteKeyGroups() { } @NotNull - public List getMissedKeyGroups() { + public List getMissedKeyGroups() { return this.missedKeyGroups; } @Override public void parse(BserValues values) throws IOException { - this.seq = values.optInt(1); - this.state = values.optBytes(2); - this.date = values.optLong(3); + this.date = values.optLong(1); List _obsoleteKeyGroups = new ArrayList(); - for (int i = 0; i < values.getRepeatedCount(4); i ++) { + for (int i = 0; i < values.getRepeatedCount(2); i ++) { _obsoleteKeyGroups.add(new ApiKeyGroupId()); } - this.obsoleteKeyGroups = values.getRepeatedObj(4, _obsoleteKeyGroups); - List _missedKeyGroups = new ArrayList(); - for (int i = 0; i < values.getRepeatedCount(5); i ++) { - _missedKeyGroups.add(new ApiKeyGroupId()); + this.obsoleteKeyGroups = values.getRepeatedObj(2, _obsoleteKeyGroups); + List _missedKeyGroups = new ArrayList(); + for (int i = 0; i < values.getRepeatedCount(3); i ++) { + _missedKeyGroups.add(new ApiKeyGroupHolder()); } - this.missedKeyGroups = values.getRepeatedObj(5, _missedKeyGroups); + this.missedKeyGroups = values.getRepeatedObj(3, _missedKeyGroups); } @Override public void serialize(BserWriter writer) throws IOException { - if (this.seq != null) { - writer.writeInt(1, this.seq); - } - if (this.state != null) { - writer.writeBytes(2, this.state); - } if (this.date != null) { - writer.writeLong(3, this.date); + writer.writeLong(1, this.date); } - writer.writeRepeatedObj(4, this.obsoleteKeyGroups); - writer.writeRepeatedObj(5, this.missedKeyGroups); + writer.writeRepeatedObj(2, this.obsoleteKeyGroups); + writer.writeRepeatedObj(3, this.missedKeyGroups); } @Override diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestEnterGroup.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateChatDropCache.java similarity index 65% rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestEnterGroup.java rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateChatDropCache.java index 136c20261f..9a3e82b244 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestEnterGroup.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateChatDropCache.java @@ -1,4 +1,4 @@ -package im.actor.core.api.rpc; +package im.actor.core.api.updates; /* * Generated by the Actor API Scheme generator. DO NOT EDIT! */ @@ -15,31 +15,31 @@ import java.util.ArrayList; import im.actor.core.api.*; -public class RequestEnterGroup extends Request { +public class UpdateChatDropCache extends Update { - public static final int HEADER = 0xc7; - public static RequestEnterGroup fromBytes(byte[] data) throws IOException { - return Bser.parse(new RequestEnterGroup(), data); + public static final int HEADER = 0xa82; + public static UpdateChatDropCache fromBytes(byte[] data) throws IOException { + return Bser.parse(new UpdateChatDropCache(), data); } - private ApiGroupOutPeer peer; + private ApiPeer peer; - public RequestEnterGroup(@NotNull ApiGroupOutPeer peer) { + public UpdateChatDropCache(@NotNull ApiPeer peer) { this.peer = peer; } - public RequestEnterGroup() { + public UpdateChatDropCache() { } @NotNull - public ApiGroupOutPeer getPeer() { + public ApiPeer getPeer() { return this.peer; } @Override public void parse(BserValues values) throws IOException { - this.peer = values.getObj(1, new ApiGroupOutPeer()); + this.peer = values.getObj(1, new ApiPeer()); } @Override @@ -52,7 +52,7 @@ public void serialize(BserWriter writer) throws IOException { @Override public String toString() { - String res = "rpc EnterGroup{"; + String res = "update ChatDropCache{"; res += "peer=" + this.peer; res += "}"; return res; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateChatGroupsChanged.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateChatGroupsChanged.java index 715757376f..2e88bbef18 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateChatGroupsChanged.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateChatGroupsChanged.java @@ -54,7 +54,7 @@ public void serialize(BserWriter writer) throws IOException { @Override public String toString() { String res = "update ChatGroupsChanged{"; - res += "dialogs=" + this.dialogs; + res += "dialogs=" + this.dialogs.size(); res += "}"; return res; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestUnregisterPush.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupDeleted.java similarity index 56% rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestUnregisterPush.java rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupDeleted.java index 29f0c521a0..f1603eb091 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/rpc/RequestUnregisterPush.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupDeleted.java @@ -1,4 +1,4 @@ -package im.actor.core.api.rpc; +package im.actor.core.api.updates; /* * Generated by the Actor API Scheme generator. DO NOT EDIT! */ @@ -15,29 +15,41 @@ import java.util.ArrayList; import im.actor.core.api.*; -public class RequestUnregisterPush extends Request { +public class UpdateGroupDeleted extends Update { - public static final int HEADER = 0x34; - public static RequestUnregisterPush fromBytes(byte[] data) throws IOException { - return Bser.parse(new RequestUnregisterPush(), data); + public static final int HEADER = 0xa62; + public static UpdateGroupDeleted fromBytes(byte[] data) throws IOException { + return Bser.parse(new UpdateGroupDeleted(), data); } + private int groupId; - public RequestUnregisterPush() { + public UpdateGroupDeleted(int groupId) { + this.groupId = groupId; + } + + public UpdateGroupDeleted() { + + } + public int getGroupId() { + return this.groupId; } @Override public void parse(BserValues values) throws IOException { + this.groupId = values.getInt(1); } @Override public void serialize(BserWriter writer) throws IOException { + writer.writeInt(1, this.groupId); } @Override public String toString() { - String res = "rpc UnregisterPush{"; + String res = "update GroupDeleted{"; + res += "groupId=" + this.groupId; res += "}"; return res; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupCanSendMessagesChanged.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupFullPermissionsChanged.java similarity index 60% rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupCanSendMessagesChanged.java rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupFullPermissionsChanged.java index dad4c9cf8f..a539daf72e 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupCanSendMessagesChanged.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupFullPermissionsChanged.java @@ -15,22 +15,22 @@ import java.util.ArrayList; import im.actor.core.api.*; -public class UpdateGroupCanSendMessagesChanged extends Update { +public class UpdateGroupFullPermissionsChanged extends Update { - public static final int HEADER = 0xa40; - public static UpdateGroupCanSendMessagesChanged fromBytes(byte[] data) throws IOException { - return Bser.parse(new UpdateGroupCanSendMessagesChanged(), data); + public static final int HEADER = 0xa68; + public static UpdateGroupFullPermissionsChanged fromBytes(byte[] data) throws IOException { + return Bser.parse(new UpdateGroupFullPermissionsChanged(), data); } private int groupId; - private boolean canSendMessages; + private long permissions; - public UpdateGroupCanSendMessagesChanged(int groupId, boolean canSendMessages) { + public UpdateGroupFullPermissionsChanged(int groupId, long permissions) { this.groupId = groupId; - this.canSendMessages = canSendMessages; + this.permissions = permissions; } - public UpdateGroupCanSendMessagesChanged() { + public UpdateGroupFullPermissionsChanged() { } @@ -38,27 +38,27 @@ public int getGroupId() { return this.groupId; } - public boolean canSendMessages() { - return this.canSendMessages; + public long getPermissions() { + return this.permissions; } @Override public void parse(BserValues values) throws IOException { this.groupId = values.getInt(1); - this.canSendMessages = values.getBool(2); + this.permissions = values.getLong(2); } @Override public void serialize(BserWriter writer) throws IOException { writer.writeInt(1, this.groupId); - writer.writeBool(2, this.canSendMessages); + writer.writeLong(2, this.permissions); } @Override public String toString() { - String res = "update GroupCanSendMessagesChanged{"; + String res = "update GroupFullPermissionsChanged{"; res += "groupId=" + this.groupId; - res += ", canSendMessages=" + this.canSendMessages; + res += ", permissions=" + this.permissions; res += "}"; return res; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupInvite.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupInvite.java deleted file mode 100644 index 108bc22396..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupInvite.java +++ /dev/null @@ -1,88 +0,0 @@ -package im.actor.core.api.updates; -/* - * Generated by the Actor API Scheme generator. DO NOT EDIT! - */ - -import im.actor.runtime.bser.*; -import im.actor.runtime.collections.*; -import static im.actor.runtime.bser.Utils.*; -import im.actor.core.network.parser.*; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.NotNull; -import com.google.j2objc.annotations.ObjectiveCName; -import java.io.IOException; -import java.util.List; -import java.util.ArrayList; -import im.actor.core.api.*; - -public class UpdateGroupInvite extends Update { - - public static final int HEADER = 0x24; - public static UpdateGroupInvite fromBytes(byte[] data) throws IOException { - return Bser.parse(new UpdateGroupInvite(), data); - } - - private int groupId; - private long rid; - private int inviteUid; - private long date; - - public UpdateGroupInvite(int groupId, long rid, int inviteUid, long date) { - this.groupId = groupId; - this.rid = rid; - this.inviteUid = inviteUid; - this.date = date; - } - - public UpdateGroupInvite() { - - } - - public int getGroupId() { - return this.groupId; - } - - public long getRid() { - return this.rid; - } - - public int getInviteUid() { - return this.inviteUid; - } - - public long getDate() { - return this.date; - } - - @Override - public void parse(BserValues values) throws IOException { - this.groupId = values.getInt(1); - this.rid = values.getLong(9); - this.inviteUid = values.getInt(5); - this.date = values.getLong(8); - } - - @Override - public void serialize(BserWriter writer) throws IOException { - writer.writeInt(1, this.groupId); - writer.writeLong(9, this.rid); - writer.writeInt(5, this.inviteUid); - writer.writeLong(8, this.date); - } - - @Override - public String toString() { - String res = "update GroupInvite{"; - res += "groupId=" + this.groupId; - res += ", rid=" + this.rid; - res += ", inviteUid=" + this.inviteUid; - res += ", date=" + this.date; - res += "}"; - return res; - } - - @Override - public int getHeaderKey() { - return HEADER; - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupMemberDiff.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupMemberDiff.java index 1407d373d6..b2d2a7def4 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupMemberDiff.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupMemberDiff.java @@ -22,11 +22,13 @@ public static UpdateGroupMemberDiff fromBytes(byte[] data) throws IOException { return Bser.parse(new UpdateGroupMemberDiff(), data); } + private int groupId; private List removedUsers; private List addedMembers; private int membersCount; - public UpdateGroupMemberDiff(@NotNull List removedUsers, @NotNull List addedMembers, int membersCount) { + public UpdateGroupMemberDiff(int groupId, @NotNull List removedUsers, @NotNull List addedMembers, int membersCount) { + this.groupId = groupId; this.removedUsers = removedUsers; this.addedMembers = addedMembers; this.membersCount = membersCount; @@ -36,6 +38,10 @@ public UpdateGroupMemberDiff() { } + public int getGroupId() { + return this.groupId; + } + @NotNull public List getRemovedUsers() { return this.removedUsers; @@ -52,20 +58,22 @@ public int getMembersCount() { @Override public void parse(BserValues values) throws IOException { - this.removedUsers = values.getRepeatedInt(1); + this.groupId = values.getInt(1); + this.removedUsers = values.getRepeatedInt(2); List _addedMembers = new ArrayList(); - for (int i = 0; i < values.getRepeatedCount(2); i ++) { + for (int i = 0; i < values.getRepeatedCount(3); i ++) { _addedMembers.add(new ApiMember()); } - this.addedMembers = values.getRepeatedObj(2, _addedMembers); - this.membersCount = values.getInt(3); + this.addedMembers = values.getRepeatedObj(3, _addedMembers); + this.membersCount = values.getInt(4); } @Override public void serialize(BserWriter writer) throws IOException { - writer.writeRepeatedInt(1, this.removedUsers); - writer.writeRepeatedObj(2, this.addedMembers); - writer.writeInt(3, this.membersCount); + writer.writeInt(1, this.groupId); + writer.writeRepeatedInt(2, this.removedUsers); + writer.writeRepeatedObj(3, this.addedMembers); + writer.writeInt(4, this.membersCount); } @Override diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupMembersUpdate.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupMembersUpdate.java deleted file mode 100644 index 29735d556d..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupMembersUpdate.java +++ /dev/null @@ -1,75 +0,0 @@ -package im.actor.core.api.updates; -/* - * Generated by the Actor API Scheme generator. DO NOT EDIT! - */ - -import im.actor.runtime.bser.*; -import im.actor.runtime.collections.*; -import static im.actor.runtime.bser.Utils.*; -import im.actor.core.network.parser.*; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.NotNull; -import com.google.j2objc.annotations.ObjectiveCName; -import java.io.IOException; -import java.util.List; -import java.util.ArrayList; -import im.actor.core.api.*; - -public class UpdateGroupMembersUpdate extends Update { - - public static final int HEADER = 0x2c; - public static UpdateGroupMembersUpdate fromBytes(byte[] data) throws IOException { - return Bser.parse(new UpdateGroupMembersUpdate(), data); - } - - private int groupId; - private List members; - - public UpdateGroupMembersUpdate(int groupId, @NotNull List members) { - this.groupId = groupId; - this.members = members; - } - - public UpdateGroupMembersUpdate() { - - } - - public int getGroupId() { - return this.groupId; - } - - @NotNull - public List getMembers() { - return this.members; - } - - @Override - public void parse(BserValues values) throws IOException { - this.groupId = values.getInt(1); - List _members = new ArrayList(); - for (int i = 0; i < values.getRepeatedCount(2); i ++) { - _members.add(new ApiMember()); - } - this.members = values.getRepeatedObj(2, _members); - } - - @Override - public void serialize(BserWriter writer) throws IOException { - writer.writeInt(1, this.groupId); - writer.writeRepeatedObj(2, this.members); - } - - @Override - public String toString() { - String res = "update GroupMembersUpdate{"; - res += "groupId=" + this.groupId; - res += ", members=" + this.members; - res += "}"; - return res; - } - - @Override - public int getHeaderKey() { - return HEADER; - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupCanViewMembersChanged.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupPermissionsChanged.java similarity index 59% rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupCanViewMembersChanged.java rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupPermissionsChanged.java index 8b117a5eb6..02f54ec56d 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupCanViewMembersChanged.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupPermissionsChanged.java @@ -15,22 +15,22 @@ import java.util.ArrayList; import im.actor.core.api.*; -public class UpdateGroupCanViewMembersChanged extends Update { +public class UpdateGroupPermissionsChanged extends Update { - public static final int HEADER = 0xa41; - public static UpdateGroupCanViewMembersChanged fromBytes(byte[] data) throws IOException { - return Bser.parse(new UpdateGroupCanViewMembersChanged(), data); + public static final int HEADER = 0xa67; + public static UpdateGroupPermissionsChanged fromBytes(byte[] data) throws IOException { + return Bser.parse(new UpdateGroupPermissionsChanged(), data); } private int groupId; - private boolean canViewMembers; + private long permissions; - public UpdateGroupCanViewMembersChanged(int groupId, boolean canViewMembers) { + public UpdateGroupPermissionsChanged(int groupId, long permissions) { this.groupId = groupId; - this.canViewMembers = canViewMembers; + this.permissions = permissions; } - public UpdateGroupCanViewMembersChanged() { + public UpdateGroupPermissionsChanged() { } @@ -38,27 +38,27 @@ public int getGroupId() { return this.groupId; } - public boolean canViewMembers() { - return this.canViewMembers; + public long getPermissions() { + return this.permissions; } @Override public void parse(BserValues values) throws IOException { this.groupId = values.getInt(1); - this.canViewMembers = values.getBool(2); + this.permissions = values.getLong(2); } @Override public void serialize(BserWriter writer) throws IOException { writer.writeInt(1, this.groupId); - writer.writeBool(2, this.canViewMembers); + writer.writeLong(2, this.permissions); } @Override public String toString() { - String res = "update GroupCanViewMembersChanged{"; + String res = "update GroupPermissionsChanged{"; res += "groupId=" + this.groupId; - res += ", canViewMembers=" + this.canViewMembers; + res += ", permissions=" + this.permissions; res += "}"; return res; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupCanInviteMembersChanged.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupShortNameChanged.java similarity index 58% rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupCanInviteMembersChanged.java rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupShortNameChanged.java index 473620779d..f5bcbedbf3 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupCanInviteMembersChanged.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupShortNameChanged.java @@ -15,22 +15,22 @@ import java.util.ArrayList; import im.actor.core.api.*; -public class UpdateGroupCanInviteMembersChanged extends Update { +public class UpdateGroupShortNameChanged extends Update { - public static final int HEADER = 0xa42; - public static UpdateGroupCanInviteMembersChanged fromBytes(byte[] data) throws IOException { - return Bser.parse(new UpdateGroupCanInviteMembersChanged(), data); + public static final int HEADER = 0xa44; + public static UpdateGroupShortNameChanged fromBytes(byte[] data) throws IOException { + return Bser.parse(new UpdateGroupShortNameChanged(), data); } private int groupId; - private boolean canInviteMembers; + private String shortName; - public UpdateGroupCanInviteMembersChanged(int groupId, boolean canInviteMembers) { + public UpdateGroupShortNameChanged(int groupId, @Nullable String shortName) { this.groupId = groupId; - this.canInviteMembers = canInviteMembers; + this.shortName = shortName; } - public UpdateGroupCanInviteMembersChanged() { + public UpdateGroupShortNameChanged() { } @@ -38,27 +38,30 @@ public int getGroupId() { return this.groupId; } - public boolean canInviteMembers() { - return this.canInviteMembers; + @Nullable + public String getShortName() { + return this.shortName; } @Override public void parse(BserValues values) throws IOException { this.groupId = values.getInt(1); - this.canInviteMembers = values.getBool(2); + this.shortName = values.optString(2); } @Override public void serialize(BserWriter writer) throws IOException { writer.writeInt(1, this.groupId); - writer.writeBool(2, this.canInviteMembers); + if (this.shortName != null) { + writer.writeString(2, this.shortName); + } } @Override public String toString() { - String res = "update GroupCanInviteMembersChanged{"; + String res = "update GroupShortNameChanged{"; res += "groupId=" + this.groupId; - res += ", canInviteMembers=" + this.canInviteMembers; + res += ", shortName=" + this.shortName; res += "}"; return res; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupUserInvited.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupUserInvited.java deleted file mode 100644 index 8c6d31c876..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupUserInvited.java +++ /dev/null @@ -1,97 +0,0 @@ -package im.actor.core.api.updates; -/* - * Generated by the Actor API Scheme generator. DO NOT EDIT! - */ - -import im.actor.runtime.bser.*; -import im.actor.runtime.collections.*; -import static im.actor.runtime.bser.Utils.*; -import im.actor.core.network.parser.*; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.NotNull; -import com.google.j2objc.annotations.ObjectiveCName; -import java.io.IOException; -import java.util.List; -import java.util.ArrayList; -import im.actor.core.api.*; - -public class UpdateGroupUserInvited extends Update { - - public static final int HEADER = 0x15; - public static UpdateGroupUserInvited fromBytes(byte[] data) throws IOException { - return Bser.parse(new UpdateGroupUserInvited(), data); - } - - private int groupId; - private long rid; - private int uid; - private int inviterUid; - private long date; - - public UpdateGroupUserInvited(int groupId, long rid, int uid, int inviterUid, long date) { - this.groupId = groupId; - this.rid = rid; - this.uid = uid; - this.inviterUid = inviterUid; - this.date = date; - } - - public UpdateGroupUserInvited() { - - } - - public int getGroupId() { - return this.groupId; - } - - public long getRid() { - return this.rid; - } - - public int getUid() { - return this.uid; - } - - public int getInviterUid() { - return this.inviterUid; - } - - public long getDate() { - return this.date; - } - - @Override - public void parse(BserValues values) throws IOException { - this.groupId = values.getInt(1); - this.rid = values.getLong(5); - this.uid = values.getInt(2); - this.inviterUid = values.getInt(3); - this.date = values.getLong(4); - } - - @Override - public void serialize(BserWriter writer) throws IOException { - writer.writeInt(1, this.groupId); - writer.writeLong(5, this.rid); - writer.writeInt(2, this.uid); - writer.writeInt(3, this.inviterUid); - writer.writeLong(4, this.date); - } - - @Override - public String toString() { - String res = "update GroupUserInvited{"; - res += "groupId=" + this.groupId; - res += ", rid=" + this.rid; - res += ", uid=" + this.uid; - res += ", inviterUid=" + this.inviterUid; - res += ", date=" + this.date; - res += "}"; - return res; - } - - @Override - public int getHeaderKey() { - return HEADER; - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupUserKick.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupUserKick.java deleted file mode 100644 index 8df80a04e2..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupUserKick.java +++ /dev/null @@ -1,97 +0,0 @@ -package im.actor.core.api.updates; -/* - * Generated by the Actor API Scheme generator. DO NOT EDIT! - */ - -import im.actor.runtime.bser.*; -import im.actor.runtime.collections.*; -import static im.actor.runtime.bser.Utils.*; -import im.actor.core.network.parser.*; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.NotNull; -import com.google.j2objc.annotations.ObjectiveCName; -import java.io.IOException; -import java.util.List; -import java.util.ArrayList; -import im.actor.core.api.*; - -public class UpdateGroupUserKick extends Update { - - public static final int HEADER = 0x18; - public static UpdateGroupUserKick fromBytes(byte[] data) throws IOException { - return Bser.parse(new UpdateGroupUserKick(), data); - } - - private int groupId; - private long rid; - private int uid; - private int kickerUid; - private long date; - - public UpdateGroupUserKick(int groupId, long rid, int uid, int kickerUid, long date) { - this.groupId = groupId; - this.rid = rid; - this.uid = uid; - this.kickerUid = kickerUid; - this.date = date; - } - - public UpdateGroupUserKick() { - - } - - public int getGroupId() { - return this.groupId; - } - - public long getRid() { - return this.rid; - } - - public int getUid() { - return this.uid; - } - - public int getKickerUid() { - return this.kickerUid; - } - - public long getDate() { - return this.date; - } - - @Override - public void parse(BserValues values) throws IOException { - this.groupId = values.getInt(1); - this.rid = values.getLong(5); - this.uid = values.getInt(2); - this.kickerUid = values.getInt(3); - this.date = values.getLong(4); - } - - @Override - public void serialize(BserWriter writer) throws IOException { - writer.writeInt(1, this.groupId); - writer.writeLong(5, this.rid); - writer.writeInt(2, this.uid); - writer.writeInt(3, this.kickerUid); - writer.writeLong(4, this.date); - } - - @Override - public String toString() { - String res = "update GroupUserKick{"; - res += "groupId=" + this.groupId; - res += ", rid=" + this.rid; - res += ", uid=" + this.uid; - res += ", kickerUid=" + this.kickerUid; - res += ", date=" + this.date; - res += "}"; - return res; - } - - @Override - public int getHeaderKey() { - return HEADER; - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupUserLeave.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupUserLeave.java deleted file mode 100644 index dea7e60132..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/api/updates/UpdateGroupUserLeave.java +++ /dev/null @@ -1,88 +0,0 @@ -package im.actor.core.api.updates; -/* - * Generated by the Actor API Scheme generator. DO NOT EDIT! - */ - -import im.actor.runtime.bser.*; -import im.actor.runtime.collections.*; -import static im.actor.runtime.bser.Utils.*; -import im.actor.core.network.parser.*; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.NotNull; -import com.google.j2objc.annotations.ObjectiveCName; -import java.io.IOException; -import java.util.List; -import java.util.ArrayList; -import im.actor.core.api.*; - -public class UpdateGroupUserLeave extends Update { - - public static final int HEADER = 0x17; - public static UpdateGroupUserLeave fromBytes(byte[] data) throws IOException { - return Bser.parse(new UpdateGroupUserLeave(), data); - } - - private int groupId; - private long rid; - private int uid; - private long date; - - public UpdateGroupUserLeave(int groupId, long rid, int uid, long date) { - this.groupId = groupId; - this.rid = rid; - this.uid = uid; - this.date = date; - } - - public UpdateGroupUserLeave() { - - } - - public int getGroupId() { - return this.groupId; - } - - public long getRid() { - return this.rid; - } - - public int getUid() { - return this.uid; - } - - public long getDate() { - return this.date; - } - - @Override - public void parse(BserValues values) throws IOException { - this.groupId = values.getInt(1); - this.rid = values.getLong(4); - this.uid = values.getInt(2); - this.date = values.getLong(3); - } - - @Override - public void serialize(BserWriter writer) throws IOException { - writer.writeInt(1, this.groupId); - writer.writeLong(4, this.rid); - writer.writeInt(2, this.uid); - writer.writeLong(3, this.date); - } - - @Override - public String toString() { - String res = "update GroupUserLeave{"; - res += "groupId=" + this.groupId; - res += ", rid=" + this.rid; - res += ", uid=" + this.uid; - res += ", date=" + this.date; - res += "}"; - return res; - } - - @Override - public int getHeaderKey() { - return HEADER; - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ContentDescription.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ContentDescription.java index 9452bee01f..aa1677b925 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ContentDescription.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ContentDescription.java @@ -99,6 +99,8 @@ public static ContentDescription fromContent(AbsContent msg) { } else if (msg instanceof ServiceContent) { return new ContentDescription(ContentType.SERVICE, ((ServiceContent) msg).getCompatText(), 0, false); + } else if (msg instanceof JsonContent) { + return new ContentDescription(ContentType.TEXT, ((JsonContent) msg).getContentDescription()); } else { return new ContentDescription(ContentType.UNKNOWN_CONTENT); } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ConversationState.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ConversationState.java index 05fd6b03c5..83e458408a 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ConversationState.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/ConversationState.java @@ -135,6 +135,7 @@ public void parse(BserValues values) throws IOException { outReadDate = values.getLong(5, 0); unreadCount = values.getInt(6); outSendDate = values.getLong(7, 0); + inMaxMessageDate = values.getLong(9, 0); } @Override @@ -147,6 +148,7 @@ public void serialize(BserWriter writer) throws IOException { writer.writeLong(5, outReadDate); writer.writeInt(6, unreadCount); writer.writeLong(7, outSendDate); + writer.writeLong(9, inMaxMessageDate); } @Override diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Dialog.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Dialog.java index 8e98424e6a..e2ecc7a94c 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Dialog.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Dialog.java @@ -50,6 +50,13 @@ public Dialog createInstance() { @SuppressWarnings("NullableProblems") @Property("readonly, nonatomic") private String dialogTitle; + @Nullable + @Property("readonly, nonatomic") + private Avatar dialogAvatar; + @Property("readonly, nonatomic") + private boolean isBot; + @Property("readonly, nonatomic") + private boolean isChannel; @Property("readonly, nonatomic") private int unreadCount; @@ -82,14 +89,12 @@ public Dialog createInstance() { private int relatedUid; - @Nullable - @Property("readonly, nonatomic") - private Avatar dialogAvatar; - public Dialog(@NotNull Peer peer, long sortKey, @NotNull String dialogTitle, @Nullable Avatar dialogAvatar, + boolean isBot, + boolean isChannel, int unreadCount, long rid, @NotNull ContentType messageType, @@ -104,6 +109,8 @@ public Dialog(@NotNull Peer peer, this.peer = peer; this.dialogTitle = StringUtil.ellipsize(dialogTitle, MAX_LENGTH); this.dialogAvatar = dialogAvatar; + this.isBot = isBot; + this.isChannel = isChannel; this.unreadCount = unreadCount; this.rid = rid; this.sortDate = sortKey; @@ -130,6 +137,19 @@ public String getDialogTitle() { return dialogTitle; } + @Nullable + public Avatar getDialogAvatar() { + return dialogAvatar; + } + + public boolean isBot() { + return isBot; + } + + public boolean isChannel() { + return isChannel; + } + public int getUnreadCount() { return unreadCount; } @@ -164,10 +184,6 @@ public int getRelatedUid() { return relatedUid; } - @Nullable - public Avatar getDialogAvatar() { - return dialogAvatar; - } @Nullable public Long getKnownReadDate() { @@ -188,8 +204,9 @@ public boolean isReceived() { } public Dialog editPeerInfo(String title, Avatar dialogAvatar) { - return new Dialog(peer, sortDate, StringUtil.ellipsize(title, MAX_LENGTH), dialogAvatar, unreadCount, rid, messageType, text, senderId, - date, relatedUid, knownReadDate, knownReceiveDate); + return new Dialog(peer, sortDate, StringUtil.ellipsize(title, MAX_LENGTH), dialogAvatar, + isBot, isChannel, unreadCount, rid, messageType, text, senderId, date, relatedUid, + knownReadDate, knownReceiveDate); } @Override @@ -201,6 +218,8 @@ public void parse(BserValues values) throws IOException { if (av != null) { dialogAvatar = new Avatar(av); } + isBot = values.getBool(15, false); + isChannel = values.getBool(16, false); unreadCount = values.getInt(4); sortDate = values.getLong(5); @@ -224,6 +243,8 @@ public void serialize(BserWriter writer) throws IOException { if (dialogAvatar != null) { writer.writeObject(3, dialogAvatar); } + writer.writeBool(15, isBot); + writer.writeBool(16, isChannel); writer.writeInt(4, unreadCount); writer.writeLong(5, sortDate); writer.writeLong(6, rid); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/DialogBuilder.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/DialogBuilder.java index cfa9fce7de..8d1fe1f61a 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/DialogBuilder.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/DialogBuilder.java @@ -22,6 +22,8 @@ public class DialogBuilder { private int relatedUid = 0; private Long knownReadDate; private Long knownReceiveDate; + private boolean isBot; + private boolean isChannel; public DialogBuilder() { @@ -41,6 +43,8 @@ public DialogBuilder(Dialog dialog) { relatedUid = dialog.getRelatedUid(); knownReadDate = dialog.getKnownReadDate(); knownReceiveDate = dialog.getKnownReceiveDate(); + isBot = dialog.isBot(); + isChannel = dialog.isChannel(); } public DialogBuilder setPeer(Peer peer) { @@ -98,6 +102,16 @@ public DialogBuilder setDialogAvatar(Avatar avatar) { return this; } + public DialogBuilder setIsBot(boolean isBot) { + this.isBot = isBot; + return this; + } + + public DialogBuilder setIsChannel(boolean isChannel) { + this.isChannel = isChannel; + return this; + } + public DialogBuilder updateKnownReadDate(Long knownReadDate) { if (knownReadDate != null && (this.knownReadDate == null || this.knownReadDate < knownReadDate)) { this.knownReadDate = knownReadDate; @@ -113,7 +127,7 @@ public DialogBuilder updateKnownReceiveDate(Long knownReceiveDate) { } public Dialog createDialog() { - return new Dialog(peer, sortKey, dialogTitle, dialogAvatar, unreadCount, rid, messageType, - text, senderId, time, relatedUid, knownReadDate, knownReceiveDate); + return new Dialog(peer, sortKey, dialogTitle, dialogAvatar, isBot, isChannel, unreadCount, + rid, messageType, text, senderId, time, relatedUid, knownReadDate, knownReceiveDate); } } \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/EntityConverter.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/EntityConverter.java index 2e47e22231..14528bdfe2 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/EntityConverter.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/EntityConverter.java @@ -32,7 +32,7 @@ public static MessageState convert(ApiMessageState state) { } public static Group convert(ApiGroup group) { - return new Group(group); + return new Group(group, null); } public static PeerType convert(ApiPeerType peerType) { diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Group.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Group.java index 21f0af7884..6b16f783f9 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Group.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Group.java @@ -14,19 +14,30 @@ import java.util.List; import im.actor.core.api.ApiAvatar; +import im.actor.core.api.ApiFullUser; import im.actor.core.api.ApiGroup; +import im.actor.core.api.ApiGroupFull; +import im.actor.core.api.ApiGroupFullPermissions; +import im.actor.core.api.ApiGroupPermissions; +import im.actor.core.api.ApiMapValue; import im.actor.core.api.ApiMember; +import im.actor.core.util.BitMaskUtil; import im.actor.runtime.bser.BserCreator; import im.actor.runtime.bser.BserValues; import im.actor.runtime.bser.BserWriter; import im.actor.runtime.storage.KeyValueItem; -public class Group extends WrapperEntity implements KeyValueItem { +public class Group extends WrapperExtEntity implements KeyValueItem { private static final int RECORD_ID = 10; + private static final int RECORD_EXT_ID = 11; public static BserCreator CREATOR = Group::new; + // + // Main + // + @Property("readonly, nonatomic") private int groupId; private long accessHash; @@ -38,35 +49,102 @@ public class Group extends WrapperEntity implements KeyValueItem { @Property("readonly, nonatomic") private Avatar avatar; @Property("readonly, nonatomic") - private int creatorId; - @Property("readonly, nonatomic") private boolean isHidden; + @Property("readonly, nonatomic") + private int membersCount; + @Property("readonly, nonatomic") + private boolean isMember; + @Property("readonly, nonatomic") + private boolean isCanSendMessage; + @Property("readonly, nonatomic") + private boolean isCanClear; + @Property("readonly, nonatomic") + private boolean isCanLeave; + @Property("readonly, nonatomic") + private boolean isCanDelete; + @Property("readonly, nonatomic") + private boolean isDeleted; + @Property("readonly, nonatomic") + private boolean isCanJoin; + @Property("readonly, nonatomic") + private boolean isCanViewInfo; + @Property("readonly, nonatomic") + private ApiMapValue ext; + @NotNull + @Property("readonly, nonatomic") + @SuppressWarnings("NullableProblems") + private GroupType groupType; + + // + // Ext + // + + @Property("readonly, nonatomic") + private Integer ownerId; @Nullable @Property("readonly, nonatomic") - private String theme; + private String topic; @Nullable @Property("readonly, nonatomic") private String about; + @Nullable + @Property("readonly, nonatomic") + private String shortName; @NotNull @Property("readonly, nonatomic") @SuppressWarnings("NullableProblems") private List members; + @Property("readonly, nonatomic") + private boolean isAsyncMembers; + @Property("readonly, nonatomic") + private boolean isCanInviteMembers; + @Property("readonly, nonatomic") + private boolean isCanInviteViaLink; + @Property("readonly, nonatomic") + private boolean isCanCall; + @Property("readonly, nonatomic") + private boolean isCanViewMembers; + @Property("readonly, nonatomic") + private boolean isSharedHistory; + @Property("readonly, nonatomic") + private boolean isCanEditInfo; + @Property("readonly, nonatomic") + private boolean isCanEditAdministration; + @Property("readonly, nonatomic") + private boolean isCanViewAdmins; + @Property("readonly, nonatomic") + private boolean isCanEditAdmins; + @Property("readonly, nonatomic") + private boolean isCanKickInvited; + @Property("readonly, nonatomic") + private boolean isCanKickAnyone; + @Property("readonly, nonatomic") + private boolean isCanEditForeign; + @Property("readonly, nonatomic") + private boolean isCanDeleteForeign; + + @Property("readonly, nonatomic") + private boolean haveExtension; - public Group(@NotNull ApiGroup group) { - super(RECORD_ID, group); + // + // Constructors + // + + public Group(@NotNull ApiGroup group, @Nullable ApiGroupFull ext) { + super(RECORD_ID, RECORD_EXT_ID, group, ext); } public Group(@NotNull byte[] data) throws IOException { - super(RECORD_ID, data); + super(RECORD_ID, RECORD_EXT_ID, data); } private Group() { - super(RECORD_ID); + super(RECORD_ID, RECORD_EXT_ID); } - public Peer peer() { - return new Peer(PeerType.GROUP, groupId); - } + // + // Getters + // public int getGroupId() { return groupId; @@ -86,14 +164,67 @@ public Avatar getAvatar() { return avatar; } + public boolean isHidden() { + return isHidden; + } + + public int getMembersCount() { + return membersCount; + } + + public boolean isMember() { + return isMember; + } + + public boolean isDeleted() { + return isDeleted; + } + + public boolean isCanSendMessage() { + return isCanSendMessage; + } + + public boolean isCanClear() { + return isCanClear; + } + + public boolean isCanLeave() { + return isCanLeave; + } + + public boolean isCanDelete() { + return isCanDelete; + } + + public boolean isCanCall() { + return isCanCall; + } + + public boolean isCanJoin() { + return isCanJoin; + } + + public boolean isCanViewInfo() { + return isCanViewInfo; + } + + @NotNull + public GroupType getGroupType() { + return groupType; + } + + // + // Ext + // + @NotNull public List getMembers() { return members; } @Nullable - public String getTheme() { - return theme; + public String getTopic() { + return topic; } @Nullable @@ -101,101 +232,139 @@ public String getAbout() { return about; } - public int getCreatorId() { - return creatorId; + @Nullable + public String getShortName() { + return shortName; } - public boolean isHidden() { - return isHidden; + @Nullable + public Integer getOwnerId() { + return ownerId; + } + + public boolean isAsyncMembers() { + return isAsyncMembers; + } + + public boolean isCanInviteMembers() { + return isCanInviteMembers; + } + + public boolean isCanViewMembers() { + return isCanViewMembers; } - public Group clearMembers() { + public boolean isCanEditInfo() { + return isCanEditInfo; + } + + public boolean isSharedHistory() { + return isSharedHistory; + } + + public boolean isHaveExtension() { + return haveExtension; + } + + public boolean isCanEditAdministration() { + return isCanEditAdministration; + } + + public boolean isCanViewAdmins() { + return isCanViewAdmins; + } + + public boolean isCanEditAdmins() { + return isCanEditAdmins; + } + + public boolean isCanInviteViaLink() { + return isCanInviteViaLink; + } + + public boolean isCanKickInvited() { + return isCanKickInvited; + } + + public boolean isCanKickAnyone() { + return isCanKickAnyone; + } + + public boolean isCanEditForeign() { + return isCanEditForeign; + } + + public boolean isCanDeleteForeign() { + return isCanDeleteForeign; + } + + public ApiMapValue getExt() { + return ext; + } + + public Group updateExt(@Nullable ApiGroupFull ext) { + return new Group(getWrapped(), ext); + } + + // + // Editing Main + // + + public Group editTitle(String title) { ApiGroup w = getWrapped(); ApiGroup res = new ApiGroup( w.getId(), w.getAccessHash(), - w.getTitle(), + title, w.getAvatar(), w.getMembersCount(), w.isMember(), w.isHidden(), w.getGroupType(), - w.canSendMessage(), - w.getExt(), - - w.isAdmin(), - w.getCreatorUid(), - new ArrayList<>(), - w.getCreateDate(), - w.getTheme(), - w.getAbout()); + w.getPermissions(), + w.isDeleted(), + w.getExt()); res.setUnmappedObjects(w.getUnmappedObjects()); - return new Group(res); + return new Group(res, getWrappedExt()); } - public Group removeMember(int uid) { + public Group editAvatar(ApiAvatar avatar) { ApiGroup w = getWrapped(); - ArrayList nMembers = new ArrayList<>(); - for (ApiMember member : w.getMembers()) { - if (member.getUid() != uid) { - nMembers.add(member); - } - } - ApiGroup res = new ApiGroup( w.getId(), w.getAccessHash(), w.getTitle(), - w.getAvatar(), + avatar, w.getMembersCount(), w.isMember(), w.isHidden(), w.getGroupType(), - w.canSendMessage(), - w.getExt(), - - w.isAdmin(), - w.getCreatorUid(), - nMembers, - w.getCreateDate(), - w.getTheme(), - w.getAbout()); + w.getPermissions(), + w.isDeleted(), + w.getExt()); res.setUnmappedObjects(w.getUnmappedObjects()); - return new Group(res); + return new Group(res, getWrappedExt()); } - public Group addMember(int uid, int inviterUid, long inviteDate) { + public Group editIsMember(boolean isMember) { ApiGroup w = getWrapped(); - ArrayList nMembers = new ArrayList<>(); - for (ApiMember member : w.getMembers()) { - if (member.getUid() != uid) { - nMembers.add(member); - } - } - nMembers.add(new ApiMember(uid, inviterUid, inviteDate, null)); ApiGroup res = new ApiGroup( w.getId(), w.getAccessHash(), w.getTitle(), w.getAvatar(), w.getMembersCount(), - w.isMember(), + isMember, w.isHidden(), w.getGroupType(), - w.canSendMessage(), - w.getExt(), - - w.isAdmin(), - w.getCreatorUid(), - nMembers, - w.getCreateDate(), - w.getTheme(), - w.getAbout()); + w.getPermissions(), + w.isDeleted(), + w.getExt()); res.setUnmappedObjects(w.getUnmappedObjects()); - return new Group(res); + return new Group(res, getWrappedExt()); } - public Group updateMembers(List nMembers) { + public Group editIsDeleted(boolean isDeleted) { ApiGroup w = getWrapped(); ApiGroup res = new ApiGroup( w.getId(), @@ -206,44 +375,32 @@ public Group updateMembers(List nMembers) { w.isMember(), w.isHidden(), w.getGroupType(), - w.canSendMessage(), - w.getExt(), - - w.isAdmin(), - w.getCreatorUid(), - nMembers, - w.getCreateDate(), - w.getTheme(), - w.getAbout()); + w.getPermissions(), + isDeleted, + w.getExt()); res.setUnmappedObjects(w.getUnmappedObjects()); - return new Group(res); + return new Group(res, getWrappedExt()); } - public Group editTitle(String title) { + public Group editPermissions(long permissions) { ApiGroup w = getWrapped(); ApiGroup res = new ApiGroup( w.getId(), w.getAccessHash(), - title, + w.getTitle(), w.getAvatar(), w.getMembersCount(), w.isMember(), w.isHidden(), w.getGroupType(), - w.canSendMessage(), - w.getExt(), - - w.isAdmin(), - w.getCreatorUid(), - w.getMembers(), - w.getCreateDate(), - w.getTheme(), - w.getAbout()); + permissions, + w.isDeleted(), + w.getExt()); res.setUnmappedObjects(w.getUnmappedObjects()); - return new Group(res); + return new Group(res, getWrappedExt()); } - public Group editTheme(String theme) { + public Group editExt(ApiMapValue ext) { ApiGroup w = getWrapped(); ApiGroup res = new ApiGroup( w.getId(), @@ -254,101 +411,453 @@ public Group editTheme(String theme) { w.isMember(), w.isHidden(), w.getGroupType(), - w.canSendMessage(), - w.getExt(), - - w.isAdmin(), - w.getCreatorUid(), - w.getMembers(), - w.getCreateDate(), - theme, - w.getAbout()); + w.getPermissions(), + w.isDeleted(), + ext); res.setUnmappedObjects(w.getUnmappedObjects()); - return new Group(res); + return new Group(res, getWrappedExt()); } - public Group editAbout(String about) { + + // + // Members + // + + public Group editMembers(List members) { + ApiGroupFull fullExt = null; + if (getWrappedExt() != null) { + ApiGroupFull e = getWrappedExt(); + fullExt = new ApiGroupFull(e.getId(), + e.getCreateDate(), + e.getOwnerUid(), + members, + e.getTheme(), + e.getAbout(), + e.getExt(), + e.isAsyncMembers(), + e.isSharedHistory(), + e.getShortName(), + e.getPermissions()); + fullExt.setUnmappedObjects(e.getUnmappedObjects()); + } + ApiGroup w = getWrapped(); ApiGroup res = new ApiGroup( w.getId(), w.getAccessHash(), w.getTitle(), w.getAvatar(), - w.getMembersCount(), + members.size(), w.isMember(), w.isHidden(), w.getGroupType(), - w.canSendMessage(), - w.getExt(), - - w.isAdmin(), - w.getCreatorUid(), - w.getMembers(), - w.getCreateDate(), - w.getTheme(), - about); - res.setUnmappedObjects(w.getUnmappedObjects()); - return new Group(res); + w.getPermissions(), + w.isDeleted(), + w.getExt()); + + return new Group(res, fullExt); } - public Group editAvatar(ApiAvatar avatar) { + public Group editMembers(List added, List removed, int count) { + ApiGroupFull fullExt = null; + if (getWrappedExt() != null) { + ApiGroupFull e = getWrappedExt(); + + ArrayList nMembers = new ArrayList<>(e.getMembers()); + + // Remove members + for (Integer i : removed) { + for (ApiMember m : nMembers) { + if (m.getUid() == i) { + nMembers.remove(m); + break; + } + } + } + // Adding members + for (ApiMember a : added) { + for (ApiMember m : nMembers) { + if (m.getUid() == a.getUid()) { + nMembers.remove(m); + break; + } + } + nMembers.add(a); + } + + fullExt = new ApiGroupFull(e.getId(), + e.getCreateDate(), + e.getOwnerUid(), + nMembers, + e.getTheme(), + e.getAbout(), + e.getExt(), + e.isAsyncMembers(), + e.isSharedHistory(), + e.getShortName(), + e.getPermissions()); + fullExt.setUnmappedObjects(e.getUnmappedObjects()); + } + ApiGroup w = getWrapped(); ApiGroup res = new ApiGroup( w.getId(), w.getAccessHash(), w.getTitle(), - avatar, - w.getMembersCount(), + w.getAvatar(), + count, w.isMember(), w.isHidden(), w.getGroupType(), - w.canSendMessage(), - w.getExt(), - - w.isAdmin(), - w.getCreatorUid(), - w.getMembers(), - w.getCreateDate(), - w.getTheme(), - w.getAbout()); + w.getPermissions(), + w.isDeleted(), + w.getExt()); + + return new Group(res, fullExt); + } + + public Group editMembersCount(int membersCount) { + ApiGroup w = getWrapped(); + ApiGroup res = new ApiGroup( + w.getId(), + w.getAccessHash(), + w.getTitle(), + w.getAvatar(), + membersCount, + w.isMember(), + w.isHidden(), + w.getGroupType(), + w.getPermissions(), + w.isDeleted(), + w.getExt()); res.setUnmappedObjects(w.getUnmappedObjects()); - return new Group(res); + return new Group(res, getWrappedExt()); + } + + public Group editMembersBecameAsync() { + if (getWrappedExt() != null) { + ApiGroupFull e = getWrappedExt(); + ApiGroupFull fullExt = new ApiGroupFull(e.getId(), + e.getCreateDate(), + e.getOwnerUid(), + new ArrayList<>(), + e.getTheme(), + e.getAbout(), + e.getExt(), + true, + e.isSharedHistory(), + e.getShortName(), + e.getPermissions()); + fullExt.setUnmappedObjects(e.getUnmappedObjects()); + + return new Group(getWrapped(), fullExt); + } else { + return this; + } + } + + public Group editMemberChangedAdmin(int uid, Boolean isAdmin) { + if (getWrappedExt() != null) { + ApiGroupFull e = getWrappedExt(); + + ArrayList nMembers = new ArrayList<>(e.getMembers()); + for (int i = 0; i < nMembers.size(); i++) { + ApiMember m = nMembers.get(i); + if (m.getUid() == uid) { + nMembers.remove(m); + nMembers.add(i, new ApiMember(m.getUid(), m.getInviterUid(), + m.getDate(), isAdmin)); + break; + } + } + + ApiGroupFull fullExt = new ApiGroupFull(e.getId(), + e.getCreateDate(), + e.getOwnerUid(), + nMembers, + e.getTheme(), + e.getAbout(), + e.getExt(), + e.isAsyncMembers(), + e.isSharedHistory(), + e.getShortName(), + e.getPermissions()); + fullExt.setUnmappedObjects(e.getUnmappedObjects()); + + return new Group(getWrapped(), fullExt); + } else { + return this; + } + } + + // + // Editing Ext + // + + public Group editTopic(String topic) { + if (getWrappedExt() != null) { + ApiGroupFull e = getWrappedExt(); + ApiGroupFull fullExt = new ApiGroupFull(e.getId(), + e.getCreateDate(), + e.getOwnerUid(), + e.getMembers(), + topic, + e.getAbout(), + e.getExt(), + e.isAsyncMembers(), + e.isSharedHistory(), + e.getShortName(), + e.getPermissions()); + fullExt.setUnmappedObjects(e.getUnmappedObjects()); + return new Group(getWrapped(), fullExt); + } else { + return this; + } + } + + public Group editAbout(String about) { + if (getWrappedExt() != null) { + ApiGroupFull e = getWrappedExt(); + ApiGroupFull fullExt = new ApiGroupFull(e.getId(), + e.getCreateDate(), + e.getOwnerUid(), + e.getMembers(), + e.getTheme(), + about, + e.getExt(), + e.isAsyncMembers(), + e.isSharedHistory(), + e.getShortName(), + e.getPermissions()); + fullExt.setUnmappedObjects(e.getUnmappedObjects()); + return new Group(getWrapped(), fullExt); + } else { + return this; + } + } + + public Group editShortName(String shortName) { + if (getWrappedExt() != null) { + ApiGroupFull e = getWrappedExt(); + ApiGroupFull fullExt = new ApiGroupFull(e.getId(), + e.getCreateDate(), + e.getOwnerUid(), + e.getMembers(), + e.getTheme(), + e.getAbout(), + e.getExt(), + e.isAsyncMembers(), + e.isSharedHistory(), + shortName, + e.getPermissions()); + fullExt.setUnmappedObjects(e.getUnmappedObjects()); + return new Group(getWrapped(), fullExt); + } else { + return this; + } + } + + public Group editFullExt(ApiMapValue ext) { + if (getWrappedExt() != null) { + ApiGroupFull e = getWrappedExt(); + ApiGroupFull fullExt = new ApiGroupFull(e.getId(), + e.getCreateDate(), + e.getOwnerUid(), + e.getMembers(), + e.getTheme(), + e.getAbout(), + ext, + e.isAsyncMembers(), + e.isSharedHistory(), + e.getShortName(), + e.getPermissions()); + fullExt.setUnmappedObjects(e.getUnmappedObjects()); + return new Group(getWrapped(), fullExt); + } else { + return this; + } + } + + public Group editOwner(int uid) { + if (getWrappedExt() != null) { + ApiGroupFull e = getWrappedExt(); + ApiGroupFull fullExt = new ApiGroupFull(e.getId(), + e.getCreateDate(), + uid, + e.getMembers(), + e.getTheme(), + e.getAbout(), + e.getExt(), + e.isAsyncMembers(), + e.isSharedHistory(), + e.getShortName(), + e.getPermissions()); + fullExt.setUnmappedObjects(e.getUnmappedObjects()); + return new Group(getWrapped(), fullExt); + } else { + return this; + } + } + + public Group editHistoryShared() { + if (getWrappedExt() != null) { + ApiGroupFull e = getWrappedExt(); + ApiGroupFull fullExt = new ApiGroupFull(e.getId(), + e.getCreateDate(), + e.getOwnerUid(), + e.getMembers(), + e.getTheme(), + e.getAbout(), + e.getExt(), + e.isAsyncMembers(), + true, + e.getShortName(), + e.getPermissions()); + fullExt.setUnmappedObjects(e.getUnmappedObjects()); + return new Group(getWrapped(), fullExt); + } else { + return this; + } + } + + public Group editExtPermissions(long permissions) { + if (getWrappedExt() != null) { + ApiGroupFull e = getWrappedExt(); + ApiGroupFull fullExt = new ApiGroupFull(e.getId(), + e.getCreateDate(), + e.getOwnerUid(), + e.getMembers(), + e.getTheme(), + e.getAbout(), + e.getExt(), + e.isAsyncMembers(), + e.isSharedHistory(), + e.getShortName(), + permissions); + fullExt.setUnmappedObjects(e.getUnmappedObjects()); + return new Group(getWrapped(), fullExt); + } else { + return this; + } } @Override - protected void applyWrapped(@NotNull ApiGroup wrapped) { + protected void applyWrapped(@NotNull ApiGroup wrapped, @Nullable ApiGroupFull ext) { + this.groupId = wrapped.getId(); this.accessHash = wrapped.getAccessHash(); this.title = wrapped.getTitle(); this.avatar = wrapped.getAvatar() != null ? new Avatar(wrapped.getAvatar()) : null; - this.creatorId = wrapped.getCreatorUid(); - this.members = new ArrayList<>(); - for (ApiMember m : wrapped.getMembers()) { - this.members.add(new GroupMember(m.getUid(), m.getInviterUid(), m.getDate(), m.isAdmin() != null ? m.isAdmin() : m.getUid() == this.creatorId)); - } this.isHidden = wrapped.isHidden() != null ? wrapped.isHidden() : false; - this.about = wrapped.getAbout(); - this.theme = wrapped.getTheme(); - } + this.membersCount = wrapped.getMembersCount() != null ? wrapped.getMembersCount() : 0; + this.isMember = wrapped.isMember() != null ? wrapped.isMember() : true; + this.isDeleted = wrapped.isDeleted() != null ? wrapped.isDeleted() : false; + this.ext = wrapped.getExt(); - @Override - public void parse(BserValues values) throws IOException { - // Is Wrapper Layout - if (values.getBool(9, false)) { - // Parse wrapper layout - super.parse(values); + if (wrapped.getGroupType() == null) { + this.groupType = GroupType.GROUP; } else { - // Convert old layout - throw new IOException("Unsupported obsolete format"); + switch (wrapped.getGroupType()) { + case CHANNEL: + this.groupType = GroupType.CHANNEL; + break; + case GROUP: + this.groupType = GroupType.GROUP; + break; + default: + case UNSUPPORTED_VALUE: + this.groupType = GroupType.OTHER; + break; + } } - } - @Override - public void serialize(BserWriter writer) throws IOException { - // Mark as wrapper layout - writer.writeBool(9, true); - // Serialize wrapper layout - super.serialize(writer); + long permissions = 0; + if (wrapped.getPermissions() != null) { + permissions = wrapped.getPermissions(); + } + /* + # 0 - canSendMessage. Default is FALSE. + # 1 - canClear. Default is FALSE. + # 2 - canLeave. Default is FALSE. + # 3 - canDelete. Default is FALSE. + */ + this.isCanSendMessage = BitMaskUtil.getBitValue(permissions, ApiGroupPermissions.SEND_MESSAGE); + this.isCanClear = BitMaskUtil.getBitValue(permissions, ApiGroupPermissions.CLEAR); + this.isCanLeave = BitMaskUtil.getBitValue(permissions, ApiGroupPermissions.LEAVE); + this.isCanDelete = BitMaskUtil.getBitValue(permissions, ApiGroupPermissions.DELETE); + this.isCanJoin = BitMaskUtil.getBitValue(permissions, ApiGroupPermissions.JOIN); + this.isCanViewInfo = BitMaskUtil.getBitValue(permissions, ApiGroupPermissions.VIEW_INFO); + + // + // Ext + // + + if (ext != null) { + this.haveExtension = true; + this.ownerId = ext.getOwnerUid(); + this.about = ext.getAbout(); + this.topic = ext.getTheme(); + this.shortName = ext.getShortName(); + this.isAsyncMembers = ext.isAsyncMembers() != null ? ext.isAsyncMembers() : false; + this.isSharedHistory = ext.isSharedHistory() != null ? ext.isSharedHistory() : false; + + /* + # 0 - canEditInfo. Default is FALSE. + # 1 - canViewMembers. Default is FALSE. + # 2 - canInviteMembers. Default is FALSE. + # 3 - canInviteViaLink. Default is FALSE. + # 4 - canCall. Default is FALSE. + # 5 - canEditAdminSettings. Default is FALSE. + # 6 - canViewAdmins. Default is FALSE. + # 7 - canEditAdmins. Default is FALSE. + # 8 - canKickInvited. Default is FALSE. + # 9 - canKickAnyone. Default is FALSE. + # 10 - canEditForeign. Default is FALSE. + # 11 - canDeleteForeign. Default is FALSE. + */ + long fullPermissions = 0; + if (ext.getPermissions() != null) { + fullPermissions = ext.getPermissions(); + } + this.isCanEditInfo = BitMaskUtil.getBitValue(fullPermissions, ApiGroupFullPermissions.EDIT_INFO); + this.isCanViewMembers = BitMaskUtil.getBitValue(fullPermissions, ApiGroupFullPermissions.VIEW_MEMBERS); + this.isCanInviteMembers = BitMaskUtil.getBitValue(fullPermissions, ApiGroupFullPermissions.INVITE_MEMBERS); + this.isCanInviteViaLink = BitMaskUtil.getBitValue(fullPermissions, ApiGroupFullPermissions.INVITE_VIA_LINK); + this.isCanCall = BitMaskUtil.getBitValue(fullPermissions, ApiGroupFullPermissions.CALL); + this.isCanEditAdministration = BitMaskUtil.getBitValue(fullPermissions, ApiGroupFullPermissions.EDIT_ADMIN_SETTINGS); + this.isCanViewAdmins = BitMaskUtil.getBitValue(fullPermissions, ApiGroupFullPermissions.VIEW_ADMINS); + this.isCanEditAdmins = BitMaskUtil.getBitValue(fullPermissions, ApiGroupFullPermissions.EDIT_ADMINS); + this.isCanKickInvited = BitMaskUtil.getBitValue(fullPermissions, ApiGroupFullPermissions.KICK_INVITED); + this.isCanKickAnyone = BitMaskUtil.getBitValue(fullPermissions, ApiGroupFullPermissions.KICK_ANYONE); + this.isCanEditForeign = BitMaskUtil.getBitValue(fullPermissions, ApiGroupFullPermissions.EDIT_FOREIGN); + this.isCanDeleteForeign = BitMaskUtil.getBitValue(fullPermissions, ApiGroupFullPermissions.DELETE_FOREIGN); + + this.members = new ArrayList<>(); + for (ApiMember m : ext.getMembers()) { + this.members.add(new GroupMember(m.getUid(), m.getInviterUid(), m.getDate(), + m.isAdmin() != null ? m.isAdmin() : false)); + } + } else { + this.haveExtension = false; + this.ownerId = 0; + this.about = null; + this.topic = null; + this.shortName = null; + this.members = new ArrayList<>(); + this.isAsyncMembers = false; + this.isCanViewMembers = false; + this.isCanInviteMembers = false; + this.isSharedHistory = false; + this.isCanEditInfo = false; + this.isCanEditAdministration = false; + this.isCanViewAdmins = false; + this.isCanEditAdmins = false; + this.isCanInviteViaLink = false; + this.isCanKickInvited = false; + this.isCanKickAnyone = false; + this.isCanEditForeign = false; + this.isCanDeleteForeign = false; + } } @Override @@ -356,10 +865,18 @@ public long getEngineId() { return groupId; } + public Peer peer() { + return new Peer(PeerType.GROUP, groupId); + } + @Override @NotNull protected ApiGroup createInstance() { return new ApiGroup(); } + @Override + protected ApiGroupFull createExtInstance() { + return new ApiGroupFull(); + } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/GroupMembersSlice.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/GroupMembersSlice.java new file mode 100644 index 0000000000..6eb17d2497 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/GroupMembersSlice.java @@ -0,0 +1,26 @@ +package im.actor.core.entity; + +import com.google.j2objc.annotations.Property; + +import java.util.ArrayList; + +public class GroupMembersSlice { + + @Property("readonly, nonatomic") + private ArrayList members; + @Property("readonly, nonatomic") + private byte[] next; + + public GroupMembersSlice(ArrayList members, byte[] next) { + this.members = members; + this.next = next; + } + + public ArrayList getMembers() { + return members; + } + + public byte[] getNext() { + return next; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/GroupPermissions.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/GroupPermissions.java new file mode 100644 index 0000000000..6b06015165 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/GroupPermissions.java @@ -0,0 +1,111 @@ +package im.actor.core.entity; + +import com.google.j2objc.annotations.ObjectiveCName; + +import im.actor.core.api.ApiAdminSettings; +import im.actor.runtime.collections.SparseArray; + +public class GroupPermissions { + + private ApiAdminSettings settings; + + public GroupPermissions(ApiAdminSettings settings) { + this.settings = settings; + } + + + @ObjectiveCName("isShowAdminsToMembers") + public boolean isShowAdminsToMembers() { + return settings.showAdminsToMembers(); + } + + @ObjectiveCName("showAdminsToMembers:") + public void setShowAdminsToMembers(boolean showAdminsToMembers) { + SparseArray unmapped = settings.getUnmappedObjects(); + settings = new ApiAdminSettings( + showAdminsToMembers, + settings.canMembersInvite(), + settings.canMembersEditGroupInfo(), + settings.canAdminsEditGroupInfo(), + settings.showJoinLeaveMessages() + ); + settings.setUnmappedObjects(unmapped); + } + + @ObjectiveCName("isMembersCanInvite") + public boolean isMembersCanInvite() { + return settings.canMembersInvite(); + } + + @ObjectiveCName("setMembersCanInvite:") + public void setMembersCanInvite(boolean membersCanInvite) { + SparseArray unmapped = settings.getUnmappedObjects(); + settings = new ApiAdminSettings( + settings.showAdminsToMembers(), + membersCanInvite, + settings.canMembersEditGroupInfo(), + settings.canAdminsEditGroupInfo(), + settings.showJoinLeaveMessages() + ); + settings.setUnmappedObjects(unmapped); + } + + @ObjectiveCName("isMembersCanEditInfo") + public boolean isMembersCanEditInfo() { + return settings.canMembersEditGroupInfo(); + } + + @ObjectiveCName("setMembersCanEditInfo:") + public void setMembersCanEditInfo(boolean canEditInfo) { + SparseArray unmapped = settings.getUnmappedObjects(); + settings = new ApiAdminSettings( + settings.showAdminsToMembers(), + settings.canMembersInvite(), + canEditInfo, + settings.canAdminsEditGroupInfo(), + settings.showJoinLeaveMessages() + ); + settings.setUnmappedObjects(unmapped); + } + + @ObjectiveCName("isAdminsCanEditGroupInfo") + public boolean isAdminsCanEditGroupInfo() { + return settings.canAdminsEditGroupInfo(); + } + + @ObjectiveCName("setAdminsCanEditGroupInfo:") + public void setAdminsCanEditGroupInfo(boolean adminsCanEditGroupInfo) { + SparseArray unmapped = settings.getUnmappedObjects(); + settings = new ApiAdminSettings( + settings.showAdminsToMembers(), + settings.canMembersInvite(), + settings.canMembersEditGroupInfo(), + adminsCanEditGroupInfo, + settings.showJoinLeaveMessages() + ); + settings.setUnmappedObjects(unmapped); + } + + + @ObjectiveCName("isShowJoinLeaveMessages") + public boolean isShowJoinLeaveMessages() { + return settings.showJoinLeaveMessages(); + } + + @ObjectiveCName("setShowJoinLeaveMessages:") + public void setShowJoinLeaveMessages(boolean showJoinLeaveMessages) { + SparseArray unmapped = settings.getUnmappedObjects(); + settings = new ApiAdminSettings( + settings.showAdminsToMembers(), + settings.canMembersInvite(), + settings.canMembersEditGroupInfo(), + settings.canAdminsEditGroupInfo(), + showJoinLeaveMessages + ); + settings.setUnmappedObjects(unmapped); + } + + public ApiAdminSettings getApiSettings() { + return settings; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/GroupType.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/GroupType.java new file mode 100644 index 0000000000..9ab166cbf7 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/GroupType.java @@ -0,0 +1,5 @@ +package im.actor.core.entity; + +public enum GroupType { + GROUP, CHANNEL, OTHER +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Notification.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Notification.java index 0998e1ce07..6663beaa42 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Notification.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/Notification.java @@ -10,12 +10,15 @@ public class Notification { @Property("readonly, nonatomic") private Peer peer; @Property("readonly, nonatomic") + private boolean isChannel; + @Property("readonly, nonatomic") private int sender; @Property("readonly, nonatomic") private ContentDescription contentDescription; - public Notification(Peer peer, int sender, ContentDescription contentDescription) { + public Notification(Peer peer, boolean isChannel, int sender, ContentDescription contentDescription) { this.peer = peer; + this.isChannel = isChannel; this.sender = sender; this.contentDescription = contentDescription; } @@ -28,6 +31,10 @@ public int getSender() { return sender; } + public boolean isChannel() { + return isChannel; + } + public ContentDescription getContentDescription() { return contentDescription; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/PeerSearchEntity.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/PeerSearchEntity.java index 53e6fed597..91ab8db3e5 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/PeerSearchEntity.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/PeerSearchEntity.java @@ -3,55 +3,18 @@ public class PeerSearchEntity { private Peer peer; - private String title; - private String description; - private Integer membersCount; - private Long date; - private Integer creatorUid; - private Boolean isPublic; - private Boolean isJoined; + private String optMatchString; - public PeerSearchEntity(Peer peer, String title, String description, Integer membersCount, - Long date, Integer creatorUid, Boolean isPublic, Boolean isJoined) { + public PeerSearchEntity(Peer peer, String optMatchString) { this.peer = peer; - this.title = title; - this.description = description; - this.membersCount = membersCount; - this.date = date; - this.creatorUid = creatorUid; - this.isPublic = isPublic; - this.isJoined = isJoined; + this.optMatchString = optMatchString; } public Peer getPeer() { return peer; } - public String getTitle() { - return title; - } - - public String getDescription() { - return description; - } - - public Integer getMembersCount() { - return membersCount; - } - - public Long getDate() { - return date; - } - - public Integer getCreatorUid() { - return creatorUid; - } - - public Boolean isPublic() { - return isPublic; - } - - public Boolean isJoined() { - return isJoined; + public String getOptMatchString() { + return optMatchString; } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/SearchResult.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/SearchResult.java new file mode 100644 index 0000000000..4b7e25b3b8 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/SearchResult.java @@ -0,0 +1,50 @@ +package im.actor.core.entity; + +import com.google.j2objc.annotations.Property; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class SearchResult { + + @NotNull + @Property("readonly, nonatomic") + private final Peer peer; + @Nullable + @Property("readonly, nonatomic") + private final Avatar avatar; + @NotNull + @Property("readonly, nonatomic") + private final String title; + @Nullable + @Property("readonly, nonatomic") + private final String matchString; + + public SearchResult(@NotNull Peer peer, @Nullable Avatar avatar, @NotNull String title, + @Nullable String matchString) { + this.peer = peer; + this.avatar = avatar; + this.title = title; + this.matchString = matchString; + } + + @NotNull + public Peer getPeer() { + return peer; + } + + @Nullable + public Avatar getAvatar() { + return avatar; + } + + @NotNull + public String getTitle() { + return title; + } + + @Nullable + public String getMatchString() { + return matchString; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/User.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/User.java index 5623dca401..2e2462fab4 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/User.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/User.java @@ -81,7 +81,6 @@ public class User extends WrapperExtEntity implements KeyV private List commands; - @NotNull @Property("readonly, nonatomic") private boolean haveExtension; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/JsonContent.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/JsonContent.java index 0da5287c73..c4a998b06b 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/JsonContent.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/entity/content/JsonContent.java @@ -2,6 +2,8 @@ import im.actor.core.api.ApiJsonMessage; import im.actor.core.entity.content.internal.ContentRemoteContainer; +import im.actor.runtime.json.JSONException; +import im.actor.runtime.json.JSONObject; public class JsonContent extends AbsContent { @@ -16,4 +18,15 @@ public JsonContent(ContentRemoteContainer contentRemoteContainer) { public String getRawJson() { return rawJson; } + + public String getContentDescription() { + String res; + try { + JSONObject data = new JSONObject(getRawJson()); + res = data.getJSONObject("data").getString("text"); + } catch (JSONException e) { + res = ""; + } + return res; + } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/i18n/I18nEngine.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/i18n/I18nEngine.java index de1516054d..1b4831bafe 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/i18n/I18nEngine.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/i18n/I18nEngine.java @@ -225,8 +225,9 @@ public String formatDialogText(Dialog dialog) { return ""; } else { String contentText = formatContentText(dialog.getSenderId(), - dialog.getMessageType(), dialog.getText(), dialog.getRelatedUid()); - if (dialog.getPeer().getPeerType() == PeerType.GROUP) { + dialog.getMessageType(), dialog.getText(), dialog.getRelatedUid(), + dialog.isChannel()); + if (dialog.getPeer().getPeerType() == PeerType.GROUP && !dialog.isChannel()) { if (!isLargeDialogMessage(dialog.getMessageType())) { return formatPerformerName(dialog.getSenderId()) + ": " + contentText; } else { @@ -279,7 +280,8 @@ public String formatNotificationText(Notification pendingNotification) { return formatContentText(pendingNotification.getSender(), pendingNotification.getContentDescription().getContentType(), pendingNotification.getContentDescription().getText(), - pendingNotification.getContentDescription().getRelatedUser()); + pendingNotification.getContentDescription().getRelatedUser(), + pendingNotification.isChannel()); } /** @@ -291,8 +293,12 @@ public String formatNotificationText(Notification pendingNotification) { * @param relatedUid optional related uid * @return formatted content */ - @ObjectiveCName("formatContentTextWithSenderId:withContentType:withText:withRelatedUid:") - public String formatContentText(int senderId, ContentType contentType, String text, int relatedUid) { + @ObjectiveCName("formatContentTextWithSenderId:withContentType:withText:withRelatedUid:withIsChannel:") + public String formatContentText(int senderId, ContentType contentType, String text, int relatedUid, + boolean isChannel) { + + String groupKey = isChannel ? "channels" : "groups"; + switch (contentType) { case TEXT: return text; @@ -323,27 +329,27 @@ public String formatContentText(int senderId, ContentType contentType, String te return getTemplateNamed(senderId, "content.service.registered.compact") .replace("{app_name}", getAppName()); case SERVICE_CREATED: - return getTemplateNamed(senderId, "content.service.groups.created"); + return getTemplateNamed(senderId, "content.service." + groupKey + ".created"); case SERVICE_ADD: - return getTemplateNamed(senderId, "content.service.groups.invited") + return getTemplateNamed(senderId, "content.service." + groupKey + ".invited") .replace("{name_added}", getSubjectName(relatedUid)); case SERVICE_LEAVE: - return getTemplateNamed(senderId, "content.service.groups.left"); + return getTemplateNamed(senderId, "content.service." + groupKey + ".left"); case SERVICE_KICK: - return getTemplateNamed(senderId, "content.service.groups.kicked") + return getTemplateNamed(senderId, "content.service." + groupKey + ".kicked") .replace("{name_kicked}", getSubjectName(relatedUid)); case SERVICE_AVATAR: - return getTemplateNamed(senderId, "content.service.groups.avatar_changed"); + return getTemplateNamed(senderId, "content.service." + groupKey + ".avatar_changed"); case SERVICE_AVATAR_REMOVED: - return getTemplateNamed(senderId, "content.service.groups.avatar_removed"); + return getTemplateNamed(senderId, "content.service." + groupKey + ".avatar_removed"); case SERVICE_TITLE: - return getTemplateNamed(senderId, "content.service.groups.title_changed.compact"); + return getTemplateNamed(senderId, "content.service." + groupKey + ".title_changed.compact"); case SERVICE_TOPIC: - return getTemplateNamed(senderId, "content.service.groups.topic_changed.compact"); + return getTemplateNamed(senderId, "content.service." + groupKey + ".topic_changed.compact"); case SERVICE_ABOUT: - return getTemplateNamed(senderId, "content.service.groups.about_changed.compact"); + return getTemplateNamed(senderId, "content.service." + groupKey + ".about_changed.compact"); case SERVICE_JOINED: - return getTemplateNamed(senderId, "content.service.groups.joined"); + return getTemplateNamed(senderId, "content.service." + groupKey + ".joined"); case SERVICE_CALL_ENDED: return get("content.service.calls.ended"); case SERVICE_CALL_MISSED: @@ -363,43 +369,44 @@ public String formatContentText(int senderId, ContentType contentType, String te * @param content content of a message * @return formatted message */ - @ObjectiveCName("formatFullServiceMessageWithSenderId:withContent:") - public String formatFullServiceMessage(int senderId, ServiceContent content) { + @ObjectiveCName("formatFullServiceMessageWithSenderId:withContent:withIsChannel:") + public String formatFullServiceMessage(int senderId, ServiceContent content, boolean isChannel) { + String groupKey = isChannel ? "channels" : "groups"; if (content instanceof ServiceUserRegistered) { return getTemplateNamed(senderId, "content.service.registered.full") .replace("{app_name}", getAppName()); } else if (content instanceof ServiceGroupCreated) { - return getTemplateNamed(senderId, "content.service.groups.created"); + return getTemplateNamed(senderId, "content.service." + groupKey + ".created"); } else if (content instanceof ServiceGroupUserInvited) { - return getTemplateNamed(senderId, "content.service.groups.invited") + return getTemplateNamed(senderId, "content.service." + groupKey + ".invited") .replace("{name_added}", getSubjectName(((ServiceGroupUserInvited) content).getAddedUid())); } else if (content instanceof ServiceGroupUserKicked) { - return getTemplateNamed(senderId, "content.service.groups.kicked") + return getTemplateNamed(senderId, "content.service." + groupKey + ".kicked") .replace("{name_kicked}", getSubjectName(((ServiceGroupUserKicked) content).getKickedUid())); } else if (content instanceof ServiceGroupUserLeave) { - return getTemplateNamed(senderId, "content.service.groups.left"); + return getTemplateNamed(senderId, "content.service." + groupKey + ".left"); } else if (content instanceof ServiceGroupTitleChanged) { - return getTemplateNamed(senderId, "content.service.groups.title_changed.full") + return getTemplateNamed(senderId, "content.service." + groupKey + ".title_changed.full") .replace("{title}", ((ServiceGroupTitleChanged) content).getNewTitle()); } else if (content instanceof ServiceGroupTopicChanged) { - return getTemplateNamed(senderId, "content.service.groups.topic_changed.full") + return getTemplateNamed(senderId, "content.service." + groupKey + ".topic_changed.full") .replace("{topic}", ((ServiceGroupTopicChanged) content).getNewTopic()); } else if (content instanceof ServiceGroupAboutChanged) { - return getTemplateNamed(senderId, "content.service.groups.about_changed.full") + return getTemplateNamed(senderId, "content.service." + groupKey + ".about_changed.full") .replace("{about}", ((ServiceGroupAboutChanged) content).getNewAbout()); } else if (content instanceof ServiceGroupAvatarChanged) { if (((ServiceGroupAvatarChanged) content).getNewAvatar() != null) { - return getTemplateNamed(senderId, "content.service.groups.avatar_changed"); + return getTemplateNamed(senderId, "content.service." + groupKey + ".avatar_changed"); } else { - return getTemplateNamed(senderId, "content.service.groups.avatar_removed"); + return getTemplateNamed(senderId, "content.service." + groupKey + ".avatar_removed"); } } else if (content instanceof ServiceGroupUserJoined) { - return getTemplateNamed(senderId, "content.service.groups.joined"); + return getTemplateNamed(senderId, "content.service." + groupKey + ".joined"); } else if (content instanceof ServiceCallEnded) { return get("content.service.calls.ended"); } else if (content instanceof ServiceCallMissed) { diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/ModuleContext.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/ModuleContext.java index bc1b76d054..8033cc6bfc 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/ModuleContext.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/ModuleContext.java @@ -7,11 +7,9 @@ import im.actor.core.modules.auth.Authentication; import im.actor.core.modules.eventbus.EventBusModule; import im.actor.core.modules.sequence.Updates; -import im.actor.core.modules.misc.AppStateModule; import im.actor.core.modules.calls.CallsModule; import im.actor.core.modules.contacts.ContactsModule; -import im.actor.core.modules.misc.DeviceInfoModule; -import im.actor.core.modules.misc.DisplayLists; +import im.actor.core.modules.conductor.DisplayLists; import im.actor.core.modules.encryption.EncryptionModule; import im.actor.core.modules.external.ExternalModule; import im.actor.core.modules.file.FilesModule; @@ -29,6 +27,7 @@ import im.actor.core.modules.storage.StorageModule; import im.actor.core.modules.typing.TypingModule; import im.actor.core.modules.users.UsersModule; +import im.actor.core.modules.conductor.ConductorModule; import im.actor.core.network.ActorApi; import im.actor.runtime.eventbus.EventBus; import im.actor.runtime.storage.PreferencesStorage; @@ -84,8 +83,6 @@ public interface ModuleContext { ProfileModule getProfileModule(); - AppStateModule getAppStateModule(); - PushesModule getPushesModule(); SecurityModule getSecurityModule(); @@ -100,9 +97,9 @@ public interface ModuleContext { MentionsModule getMentions(); - DeviceInfoModule getDeviceInfoModule(); - EncryptionModule getEncryption(); EventBusModule getEventBus(); + + ConductorModule getConductor(); } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/Modules.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/Modules.java index fd17ce01d0..cca0a8b41a 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/Modules.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/Modules.java @@ -11,11 +11,9 @@ import im.actor.core.modules.auth.Authentication; import im.actor.core.modules.eventbus.EventBusModule; import im.actor.core.modules.sequence.Updates; -import im.actor.core.modules.misc.AppStateModule; import im.actor.core.modules.calls.CallsModule; import im.actor.core.modules.contacts.ContactsModule; -import im.actor.core.modules.misc.DeviceInfoModule; -import im.actor.core.modules.misc.DisplayLists; +import im.actor.core.modules.conductor.DisplayLists; import im.actor.core.modules.encryption.EncryptionModule; import im.actor.core.modules.external.ExternalModule; import im.actor.core.modules.file.FilesModule; @@ -33,6 +31,7 @@ import im.actor.core.modules.storage.StorageModule; import im.actor.core.modules.typing.TypingModule; import im.actor.core.modules.users.UsersModule; +import im.actor.core.modules.conductor.ConductorModule; import im.actor.core.network.ActorApi; import im.actor.core.util.Timing; import im.actor.runtime.Runtime; @@ -63,7 +62,6 @@ public class Modules implements ModuleContext { private volatile Updates updates; private volatile UsersModule users; private volatile GroupsModule groups; - private volatile AppStateModule appStateModule; private volatile StickersModule stickers; private volatile CallsModule calls; private volatile MessagesModule messages; @@ -80,7 +78,7 @@ public class Modules implements ModuleContext { private volatile DisplayLists displayLists; private volatile MentionsModule mentions; private volatile EncryptionModule encryptionModule; - private volatile DeviceInfoModule deviceInfoModule; + private volatile ConductorModule conductor; private volatile EventBusModule eventBusModule; public Modules(Messenger messenger, Configuration configuration) { @@ -101,8 +99,8 @@ public Modules(Messenger messenger, Configuration configuration) { // timing.section("Events"); this.events = new EventBus(); - // timing.section("App State"); - appStateModule = new AppStateModule(this); + // timing.section("Conductor"); + this.conductor = new ConductorModule(this); // timing.section("API"); this.api = new ApiModule(this); @@ -127,14 +125,15 @@ public void run() { public void onLoggedIn(boolean first) { Timing timing = new Timing("ACCOUNT_CREATE"); + timing.section("Users"); users = new UsersModule(this); timing.section("Storage"); storageModule.run(first); timing.section("Groups"); groups = new GroupsModule(this); - timing.section("App State"); - appStateModule.run(); + timing.section("Conductor"); + conductor.run(); timing.section("Stickers"); stickers = new StickersModule(this); timing.section("Calls"); @@ -167,8 +166,6 @@ public void onLoggedIn(boolean first) { // encryptionModule = new EncryptionModule(this); timing.section("DisplayLists"); displayLists = new DisplayLists(this); - timing.section("DeviceInfo"); - deviceInfoModule = new DeviceInfoModule(this); timing.section("EventBus"); eventBusModule = new EventBusModule(this); @@ -176,18 +173,16 @@ public void onLoggedIn(boolean first) { timing = new Timing("ACCOUNT_RUN"); timing.section("Users"); users.run(); + timing.section("Groups"); + groups.run(); timing.section("Settings"); settings.run(); - timing.section("DeviceInfo"); - deviceInfoModule.run(); timing.section("Files"); filesModule.run(); timing.section("Search"); search.run(); timing.section("Notifications"); notifications.run(); - timing.section("AppState"); - appStateModule.run(); // timing.section("Encryption"); // encryptionModule.run(); timing.section("Contacts"); @@ -202,17 +197,14 @@ public void onLoggedIn(boolean first) { calls.run(); timing.section("Stickers"); stickers.run(); + timing.section("Conductor:end"); + conductor.runAfter(); timing.end(); if (Runtime.isMainThread()) { messenger.onLoggedIn(); } else { - Runtime.postToMainThread(new Runnable() { - @Override - public void run() { - messenger.onLoggedIn(); - } - }); + Runtime.postToMainThread(() -> messenger.onLoggedIn()); } } @@ -335,11 +327,6 @@ public ProfileModule getProfileModule() { return profile; } - @Override - public AppStateModule getAppStateModule() { - return appStateModule; - } - @Override public PushesModule getPushesModule() { return pushes; @@ -375,11 +362,6 @@ public MentionsModule getMentions() { return mentions; } - @Override - public DeviceInfoModule getDeviceInfoModule() { - return deviceInfoModule; - } - @Override public EncryptionModule getEncryption() { return encryptionModule; @@ -390,6 +372,11 @@ public EventBusModule getEventBus() { return eventBusModule; } + @Override + public ConductorModule getConductor() { + return conductor; + } + @Override public EventBus getEvents() { return events; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/api/ApiModule.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/api/ApiModule.java index 6fd1b13d4f..0890f8bd12 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/api/ApiModule.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/api/ApiModule.java @@ -1,5 +1,7 @@ package im.actor.core.modules.api; +import java.io.IOException; + import im.actor.core.modules.AbsModule; import im.actor.core.modules.Modules; import im.actor.core.events.AppVisibleChanged; @@ -9,10 +11,14 @@ import im.actor.core.network.ActorApiCallback; import im.actor.core.network.AuthKeyStorage; import im.actor.core.network.Endpoints; +import im.actor.core.network.TrustedKey; import im.actor.core.network.parser.Request; import im.actor.runtime.actors.ActorRef; import im.actor.runtime.eventbus.BusSubscriber; import im.actor.runtime.eventbus.Event; +import im.actor.runtime.mtproto.ConnectionEndpoint; +import im.actor.runtime.mtproto.ConnectionEndpointArray; +import im.actor.runtime.promise.Promise; import static im.actor.runtime.actors.ActorSystem.system; @@ -27,8 +33,21 @@ public ApiModule(Modules context) { this.authKeyStorage = new PreferenceApiStorage(context().getPreferences()); - this.actorApi = new ActorApi(new Endpoints(context().getConfiguration().getEndpoints(), - context().getConfiguration().getTrustedKeys()), + Endpoints endpoints = null; + byte[] customEndpointsBytes = context().getPreferences().getBytes("custom_endpoints"); + if (customEndpointsBytes != null) { + try { + endpoints = Endpoints.fromBytes(customEndpointsBytes); + } catch (IOException e) { + e.printStackTrace(); + } + } + + if (endpoints == null) { + endpoints = new Endpoints(context().getConfiguration().getEndpoints(), context().getConfiguration().getTrustedKeys()); + } + + this.actorApi = new ActorApi(endpoints, authKeyStorage, new ActorApiCallbackImpl(), context().getConfiguration().isEnableNetworkLogging(), @@ -82,6 +101,33 @@ public void performPersistCursorRequest(String name, long key, Request request) persistentRequests.send(new PersistentRequestsActor.PerformCursorRequest(name, key, request)); } + /** + * Changing endpoint + */ + public void changeEndpoint(String endpoint) throws ConnectionEndpointArray.UnknownSchemeException { + changeEndpoints(new Endpoints(new ConnectionEndpointArray().addEndpoint(endpoint).toArray(new ConnectionEndpoint[1]), new TrustedKey[0])); + } + + /** + * Changing endpoints + */ + public synchronized void changeEndpoints(Endpoints endpoints) { + context().getPreferences().putBytes("custom_endpoints", endpoints.toByteArray()); + actorApi.changeEndpoints(endpoints); + } + + /** + * Reset default endpoints + */ + public synchronized void resetToDefaultEndpoints() { + context().getPreferences().putBytes("custom_endpoints", null); + actorApi.resetToDefaultEndpoints(); + } + + public Promise checkIsCurrentAuthId(long authId) { + return actorApi.checkIsCurrentAuthId(authId); + } + @Override public void onBusEvent(Event event) { if (event instanceof AppVisibleChanged) { diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/api/ApiSupportConfiguration.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/api/ApiSupportConfiguration.java index 38f9632862..53442bee8e 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/api/ApiSupportConfiguration.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/api/ApiSupportConfiguration.java @@ -15,6 +15,7 @@ public class ApiSupportConfiguration { opts.add(ApiUpdateOptimization.STRIP_ENTITIES); opts.add(ApiUpdateOptimization.STRIP_COUNTERS); opts.add(ApiUpdateOptimization.COMPACT_USERS); + opts.add(ApiUpdateOptimization.GROUPS_V2); OPTIMIZATIONS = Collections.unmodifiableList(opts); } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/auth/Authentication.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/auth/Authentication.java index 70e8e1590a..9a06531fe8 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/auth/Authentication.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/auth/Authentication.java @@ -36,14 +36,18 @@ import im.actor.core.entity.User; import im.actor.core.modules.Modules; import im.actor.core.modules.AbsModule; +import im.actor.core.network.Endpoints; import im.actor.core.network.RpcCallback; import im.actor.core.network.RpcException; +import im.actor.core.network.TrustedKey; import im.actor.core.network.parser.Request; import im.actor.core.network.parser.Response; import im.actor.core.viewmodel.Command; import im.actor.core.viewmodel.CommandCallback; import im.actor.runtime.*; import im.actor.runtime.Runtime; +import im.actor.runtime.mtproto.ConnectionEndpoint; +import im.actor.runtime.mtproto.ConnectionEndpointArray; import im.actor.runtime.promise.Promise; import im.actor.runtime.promise.PromiseFunc; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/calls/CallActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/calls/CallActor.java index cd4a9bb38d..329b300ce3 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/calls/CallActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/calls/CallActor.java @@ -66,7 +66,8 @@ public void preStart() { callBus.joinMasterBus(responseDoCall.getEventBusId(), responseDoCall.getDeviceId()); callBus.changeVideoEnabled(isVideoInitiallyEnabled); callBus.startOwn(); - callVM = callViewModels.spawnNewOutgoingVM(responseDoCall.getCallId(), peer, isVideoInitiallyEnabled); + callVM = callViewModels.spawnNewOutgoingVM(responseDoCall.getCallId(), peer, isVideoInitiallyEnabled, + isVideoInitiallyEnabled); }).failure(e -> self().send(PoisonPill.INSTANCE)); } else { api(new RequestGetCallInfo(callId)).then(responseGetCallInfo -> { @@ -76,7 +77,8 @@ public void preStart() { isVideoInitiallyEnabled = responseGetCallInfo.isVideoPreferred(); callBus.changeVideoEnabled(isVideoInitiallyEnabled); } - callVM = callViewModels.spawnNewIncomingVM(callId, peer, isVideoInitiallyEnabled, CallState.RINGING); + callVM = callViewModels.spawnNewIncomingVM(callId, peer, isVideoInitiallyEnabled, + isVideoInitiallyEnabled, CallState.RINGING); }).failure(e -> self().send(PoisonPill.INSTANCE)); } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/calls/CallViewModels.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/calls/CallViewModels.java index 094276249c..a42a3eba3b 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/calls/CallViewModels.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/calls/CallViewModels.java @@ -27,23 +27,29 @@ public synchronized CallVM getCall(long id) { return callModels.get(id); } - public synchronized CallVM spawnNewVM(long callId, Peer peer, boolean isOutgoing, boolean isVideoEnabled, ArrayList members, CallState callState) { - CallVM callVM = new CallVM(callId, peer, isOutgoing, isVideoEnabled, members, callState); + public synchronized CallVM spawnNewVM(long callId, Peer peer, boolean isOutgoing, + boolean isVideoEnabled, boolean isVideoPreffered, + ArrayList members, CallState callState) { + CallVM callVM = new CallVM(callId, peer, isOutgoing, isVideoEnabled, isVideoPreffered, + members, callState); synchronized (callModels) { callModels.put(callId, callVM); } return callVM; } - public synchronized CallVM spawnNewIncomingVM(long callId, Peer peer, boolean isVideoEnabled, CallState callState) { - CallVM callVM = new CallVM(callId, peer, false, isVideoEnabled, new ArrayList<>(), callState); + public synchronized CallVM spawnNewIncomingVM(long callId, Peer peer, boolean isVideoEnabled, + boolean isVideoPreffered, CallState callState) { + CallVM callVM = new CallVM(callId, peer, false, isVideoEnabled, isVideoPreffered, + new ArrayList<>(), callState); synchronized (callModels) { callModels.put(callId, callVM); } return callVM; } - public synchronized CallVM spawnNewOutgoingVM(long callId, Peer peer, boolean isVideoEnabled) { + public synchronized CallVM spawnNewOutgoingVM(long callId, Peer peer, boolean isVideoEnabled, + boolean isVideoPreferred) { ArrayList members = new ArrayList<>(); if (peer.getPeerType() == PeerType.PRIVATE || peer.getPeerType() == PeerType.PRIVATE_ENCRYPTED) { @@ -56,6 +62,7 @@ public synchronized CallVM spawnNewOutgoingVM(long callId, Peer peer, boolean is } } } - return spawnNewVM(callId, peer, true, isVideoEnabled, members, CallState.RINGING); + return spawnNewVM(callId, peer, true, isVideoEnabled, isVideoPreferred, members, + CallState.RINGING); } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/conductor/Conductor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/conductor/Conductor.java new file mode 100644 index 0000000000..1cf4037965 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/conductor/Conductor.java @@ -0,0 +1,41 @@ +package im.actor.core.modules.conductor; + +import im.actor.core.modules.ModuleContext; +import im.actor.runtime.actors.ActorInterface; + +import static im.actor.runtime.actors.ActorSystem.system; + +public class Conductor extends ActorInterface { + + public Conductor(ModuleContext context) { + super(system().actorOf("conductor", () -> new ConductorActor(context))); + } + + public void finishLaunching() { + send(new ConductorActor.FinishLaunching()); + } + + public void onContactsLoaded() { + send(new ConductorActor.ContactsLoaded()); + } + + public void onDialogsLoaded() { + send(new ConductorActor.DialogsLoaded()); + } + + public void onSettingsLoaded() { + send(new ConductorActor.SettingsLoaded()); + } + + public void onDialogsChanged(boolean isEmpty) { + send(new ConductorActor.DialogsChanged(isEmpty)); + } + + public void onContactsChanged(boolean isEmpty) { + send(new ConductorActor.ContactsChanged(isEmpty)); + } + + public void onPhoneBookImported() { + send(new ConductorActor.BookImported()); + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/conductor/ConductorActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/conductor/ConductorActor.java new file mode 100644 index 0000000000..9b616db53b --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/conductor/ConductorActor.java @@ -0,0 +1,302 @@ +package im.actor.core.modules.conductor; + +import java.util.ArrayList; + +import im.actor.core.AutoJoinType; +import im.actor.core.api.rpc.RequestNotifyAboutDeviceInfo; +import im.actor.core.api.rpc.ResponseVoid; +import im.actor.core.modules.ModuleActor; +import im.actor.core.modules.ModuleContext; +import im.actor.core.util.JavaUtil; +import im.actor.core.viewmodel.AppStateVM; +import im.actor.runtime.Log; + +/** + * Launching App in stages + * 1. Download existing Contacts and Dialogs + * 2. Start Phone Book uploading + * 3. When app is fully loaded (at least one contact or message in dialogs) then invoke + * initialization actions such as joining channel + */ +public class ConductorActor extends ModuleActor { + + public static final String TAG = "Conductor"; + private static final ResponseVoid DUMB = null; + private static final Integer DUMB2 = null; + + private AppStateVM appStateVM; + private boolean isStarted = false; + + public ConductorActor(ModuleContext context) { + super(context); + } + + @Override + public void preStart() { + super.preStart(); + + updateDeviceInfoIfNeeded(); + + appStateVM = context().getConductor().getAppStateVM(); + } + + public void onFinishLaunching() { + if (isStarted) { + return; + } + isStarted = true; + unstashAll(); + + if (appStateVM.isDialogsLoaded() && appStateVM.isContactsLoaded() && appStateVM.isSettingsLoaded()) { + onInitialDataDownloaded(); + } + + if (appStateVM.getIsAppLoaded().get() || !appStateVM.getIsAppEmpty().get()) { + onAppReady(); + } + } + + // + // Initial Loading + // + + public void onDialogsLoaded() { + Log.d(TAG, "Dialogs Loaded"); + if (!appStateVM.isDialogsLoaded()) { + appStateVM.onDialogsLoaded(); + if (appStateVM.isContactsLoaded() && appStateVM.isSettingsLoaded()) { + onInitialDataDownloaded(); + } + } + } + + public void onContactsLoaded() { + Log.d(TAG, "Contacts Loaded"); + if (!appStateVM.isContactsLoaded()) { + appStateVM.onContactsLoaded(); + if (appStateVM.isDialogsLoaded() && appStateVM.isSettingsLoaded()) { + onInitialDataDownloaded(); + } + } + } + + public void onSettingsLoaded() { + Log.d(TAG, "Settings Loaded"); + if (!appStateVM.isSettingsLoaded()) { + appStateVM.onSettingsLoaded(); + if (appStateVM.isDialogsLoaded() && appStateVM.isContactsLoaded()) { + onInitialDataDownloaded(); + } + } + } + + /** + * Called after dialogs, contacts and settings are downloaded from server + */ + public void onInitialDataDownloaded() { + Log.d(TAG, "Initial Data Loaded"); + context().getContactsModule().startImport(); + if (appStateVM.isBookImported()) { + onAppLoaded(); + } + } + + public void onBookImported() { + Log.d(TAG, "Book Uploaded"); + if (!appStateVM.isBookImported()) { + appStateVM.onPhoneImported(); + onAppLoaded(); + } + } + + // + // Data Sync + // + + public void onDialogsChanged(boolean isEmpty) { + boolean wasEmpty = appStateVM.getIsAppEmpty().get(); + appStateVM.onDialogsChanged(isEmpty); + if (wasEmpty && !appStateVM.getIsAppEmpty().get()) { + onAppReady(); + } + } + + public void onContactsChanged(boolean isEmpty) { + boolean wasEmpty = appStateVM.getIsAppEmpty().get(); + appStateVM.onContactsChanged(isEmpty); + if (wasEmpty && !appStateVM.getIsAppEmpty().get()) { + onAppReady(); + } + } + + /** + * Called after dialogs, contacts and settings are downloaded from server and + * phone book is imported to server + */ + public void onAppLoaded() { + Log.d(TAG, "App Loaded"); + + // Joining Groups + if (config().getAutoJoinType() == AutoJoinType.IMMEDIATELY) { + joinGroups(); + } + } + + /** + * Called after at least one message or contact is in user's phone book + */ + public void onAppReady() { + Log.d(TAG, "App Ready"); + + // Joining Groups + if (config().getAutoJoinType() == AutoJoinType.AFTER_INIT) { + joinGroups(); + } + } + + private void joinGroups() { + for (String s : config().getAutoJoinGroups()) { + if (!context().getSettingsModule().getBooleanValue("auto_join." + s, false)) { + context().getGroupsModule().joinGroupByToken(s).then(r -> { + context().getSettingsModule().setBooleanValue("auto_join." + s, true); + }); + } + } + } + + // + // Tools + // + + public void updateDeviceInfoIfNeeded() { + + // + // Loading Information + // + ArrayList langs = new ArrayList<>(); + for (String s : context().getConfiguration().getPreferredLanguages()) { + langs.add(s); + } + final String timeZone = context().getConfiguration().getTimeZone(); + + // + // Checking if information changed + // + String expectedLangs = ""; + for (String s : langs) { + if (!"".equals(expectedLangs)) { + expectedLangs += ","; + } + expectedLangs += s.toLowerCase(); + } + + if (expectedLangs.equals(preferences().getString("device_info_langs")) && + JavaUtil.equalsE(timeZone, preferences().getString("device_info_timezone"))) { + // Already sent + return; + } + + // + // Performing Notification + // + final String finalExpectedLangs = expectedLangs; + api(new RequestNotifyAboutDeviceInfo(langs, timeZone)).then(r -> { + preferences().putString("device_info_langs", finalExpectedLangs); + preferences().putString("device_info_timezone", timeZone); + }); + } + + + // + // Messages + // + + @Override + public void onReceive(Object message) { + if (message instanceof DialogsLoaded) { + if (!isStarted) { + stash(); + return; + } + onDialogsLoaded(); + } else if (message instanceof ContactsLoaded) { + if (!isStarted) { + stash(); + return; + } + onContactsLoaded(); + } else if (message instanceof SettingsLoaded) { + if (!isStarted) { + stash(); + return; + } + onSettingsLoaded(); + } else if (message instanceof BookImported) { + if (!isStarted) { + stash(); + return; + } + onBookImported(); + } else if (message instanceof ContactsChanged) { + if (!isStarted) { + stash(); + return; + } + onContactsChanged(((ContactsChanged) message).isEmpty()); + } else if (message instanceof DialogsChanged) { + if (!isStarted) { + stash(); + return; + } + onDialogsChanged(((DialogsChanged) message).isEmpty()); + } else if (message instanceof FinishLaunching) { + onFinishLaunching(); + } else { + super.onReceive(message); + } + } + + public static class DialogsLoaded { + + } + + public static class ContactsLoaded { + + } + + public static class BookImported { + + } + + public static class SettingsLoaded { + + } + + public static class ContactsChanged { + private boolean isEmpty; + + public ContactsChanged(boolean isEmpty) { + this.isEmpty = isEmpty; + } + + public boolean isEmpty() { + return isEmpty; + } + } + + public static class DialogsChanged { + private boolean isEmpty; + + public DialogsChanged(boolean isEmpty) { + this.isEmpty = isEmpty; + } + + public boolean isEmpty() { + return isEmpty; + } + } + + public static class FinishLaunching { + + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/conductor/ConductorModule.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/conductor/ConductorModule.java new file mode 100644 index 0000000000..fb58a1c169 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/conductor/ConductorModule.java @@ -0,0 +1,40 @@ +package im.actor.core.modules.conductor; + +import im.actor.core.modules.AbsModule; +import im.actor.core.modules.ModuleContext; +import im.actor.core.viewmodel.AppStateVM; +import im.actor.core.viewmodel.GlobalStateVM; + +public class ConductorModule extends AbsModule { + + private AppStateVM appStateVM; + private GlobalStateVM globalStateVM; + private Conductor conductor; + + public ConductorModule(ModuleContext context) { + super(context); + + globalStateVM = new GlobalStateVM(context); + } + + public void run() { + this.appStateVM = new AppStateVM(context()); + this.conductor = new Conductor(context()); + } + + public void runAfter() { + this.conductor.finishLaunching(); + } + + public Conductor getConductor() { + return conductor; + } + + public AppStateVM getAppStateVM() { + return appStateVM; + } + + public GlobalStateVM getGlobalStateVM() { + return globalStateVM; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/misc/DisplayLists.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/conductor/DisplayLists.java similarity index 98% rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/misc/DisplayLists.java rename to actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/conductor/DisplayLists.java index 7bbed42328..9a5babc444 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/misc/DisplayLists.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/conductor/DisplayLists.java @@ -2,7 +2,7 @@ * Copyright (C) 2015 Actor LLC. */ -package im.actor.core.modules.misc; +package im.actor.core.modules.conductor; import java.util.HashMap; @@ -95,7 +95,7 @@ public PlatformDisplayList buildChatList(final Peer peer, boolean isSha PlatformDisplayList res = Storage.createDisplayList(context().getMessagesModule().getConversationEngine(peer), isShared, Message.ENTITY_NAME); - long lastRead = context().getMessagesModule().getConversationVM(peer).getLastMessageDate(); + long lastRead = context().getMessagesModule().getConversationVM(peer).getLastReadMessageDate(); if (lastRead != 0) { res.initCenter(lastRead); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/contacts/BookImportActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/contacts/BookImportActor.java index 2618f6bd11..4d63b49991 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/contacts/BookImportActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/contacts/BookImportActor.java @@ -41,6 +41,9 @@ public class BookImportActor extends ModuleActor { private static final int MAX_IMPORT_SIZE = 50; + // Is Started + private boolean isStarted = false; + // Reading Phone Book private boolean phoneBookReadingIsInProgress = false; @@ -71,13 +74,20 @@ public void preStart() { e.getLocalizedMessage(); } } + } - self().send(new PerformSync()); + private void start() { + if (!isStarted) { + isStarted = true; + self().send(new PerformSync()); + } } private void performSync() { // Ignoring syncing if not enabled if (!config().isEnablePhoneBookImport()) { + // Marking as everything is imported + context().getConductor().getConductor().onPhoneBookImported(); return; } @@ -167,7 +177,7 @@ private void performImportIfRequired() { Log.d(TAG, "performImportIfRequired:exiting:nothing to import"); } // Marking as everything is imported - context().getAppStateModule().onBookImported(); + context().getConductor().getConductor().onPhoneBookImported(); return; } @@ -279,14 +289,23 @@ public void onError(RpcException e) { @Override public void onReceive(Object message) { if (message instanceof PerformSync) { + if (!isStarted) { + return; + } performSync(); } else if (message instanceof PhoneBookLoaded) { onPhoneBookLoaded(((PhoneBookLoaded) message).getPhoneBook()); + } else if (message instanceof Start) { + start(); } else { super.onReceive(message); } } + public static class Start { + + } + public static class PerformSync { } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/contacts/ContactsModule.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/contacts/ContactsModule.java index 733294ba30..4a7af1c6b9 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/contacts/ContactsModule.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/contacts/ContactsModule.java @@ -58,6 +58,10 @@ public SyncKeyValue getBookImportState() { return bookImportState; } + public void startImport() { + bookImportActor.send(new BookImportActor.Start()); + } + public ListEngine getContacts() { return contacts; } @@ -82,6 +86,18 @@ public boolean isUserContact(int uid) { return preferences().getBool("contact_" + uid, false); } + public void markInPhoneBook(int uid) { + preferences().putBool("contact_in_pb_" + uid, true); + } + + public void markNotInPhoneBook(int uid) { + preferences().putBool("contact_in_pb_" + uid, false); + } + + public boolean isUserInPhoneBook(int uid) { + return preferences().getBool("contact_in_pb_" + uid, false); + } + public Promise findUsers(final String query) { return api(new RequestSearchContacts(query, ApiSupportConfiguration.OPTIMIZATIONS)) .chain(responseSearchContacts -> updates().loadRequiredPeers(responseSearchContacts.getUserPeers(), new ArrayList<>())) diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/contacts/ContactsSyncActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/contacts/ContactsSyncActor.java index 65b5a5eafd..d8aef6bcf2 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/contacts/ContactsSyncActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/contacts/ContactsSyncActor.java @@ -147,7 +147,7 @@ public void onContactsLoaded(ResponseGetContacts result) { isInProgress = false; - context().getAppStateModule().onContactsLoaded(); + context().getConductor().getConductor().onContactsLoaded(); if (result.isNotChanged()) { Log.d(TAG, "Sync: Not changed"); @@ -306,7 +306,7 @@ private void saveList() { } private void notifyState() { - context().getAppStateModule().onContactsUpdate(context().getContactsModule().getContacts().isEmpty()); + context().getConductor().getConductor().onContactsChanged(context().getContactsModule().getContacts().isEmpty()); } @Override diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/KeyManagerActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/KeyManagerActor.java index 3a0a3f97a5..9a6501c501 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/KeyManagerActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/encryption/KeyManagerActor.java @@ -367,7 +367,7 @@ public PublicKey apply(ResponsePublicKeys responsePublicKeys) { if (!Curve25519.verifySignature(keysGroup.getT1().getIdentityKey().getPublicKey(), keyHash, sig.getSignature())) { - throw new RuntimeException("Key signature does not match"); + throw new RuntimeException("Key signature does not isMatch"); } PublicKey pkey = new PublicKey(keyId, key.getKeyAlg(), key.getKeyMaterial()); @@ -417,7 +417,7 @@ public PublicKey apply(ResponsePublicKeys response) { if (!Curve25519.verifySignature(keyGroups.getT1().getIdentityKey().getPublicKey(), keyHash, sig.getSignature())) { - throw new RuntimeException("Key signature does not match"); + throw new RuntimeException("Key signature does not isMatch"); } return new PublicKey(key.getKeyId(), key.getKeyAlg(), key.getKeyMaterial()); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/GroupsModule.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/GroupsModule.java index 80530edaa4..3f4e982237 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/GroupsModule.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/GroupsModule.java @@ -8,50 +8,48 @@ import java.util.HashMap; import java.util.List; -import im.actor.core.ApiConfiguration; -import im.actor.core.api.ApiConfig; +import im.actor.core.api.ApiAdminSettings; import im.actor.core.api.ApiGroupOutPeer; -import im.actor.core.api.ApiMessageAttributes; +import im.actor.core.api.ApiGroupType; +import im.actor.core.api.ApiMember; import im.actor.core.api.ApiOutPeer; -import im.actor.core.api.ApiPeer; import im.actor.core.api.ApiPeerType; -import im.actor.core.api.ApiServiceExUserJoined; -import im.actor.core.api.ApiServiceMessage; import im.actor.core.api.ApiUserOutPeer; import im.actor.core.api.rpc.RequestCreateGroup; +import im.actor.core.api.rpc.RequestDeleteGroup; +import im.actor.core.api.rpc.RequestDismissUserAdmin; import im.actor.core.api.rpc.RequestEditGroupAbout; +import im.actor.core.api.rpc.RequestEditGroupShortName; import im.actor.core.api.rpc.RequestEditGroupTitle; import im.actor.core.api.rpc.RequestEditGroupTopic; import im.actor.core.api.rpc.RequestGetGroupInviteUrl; import im.actor.core.api.rpc.RequestGetIntegrationToken; import im.actor.core.api.rpc.RequestInviteUser; import im.actor.core.api.rpc.RequestJoinGroup; +import im.actor.core.api.rpc.RequestJoinGroupByPeer; import im.actor.core.api.rpc.RequestKickUser; +import im.actor.core.api.rpc.RequestLeaveAndDelete; import im.actor.core.api.rpc.RequestLeaveGroup; -import im.actor.core.api.rpc.RequestMakeUserAdmin; +import im.actor.core.api.rpc.RequestLoadAdminSettings; +import im.actor.core.api.rpc.RequestLoadMembers; import im.actor.core.api.rpc.RequestMakeUserAdminObsolete; import im.actor.core.api.rpc.RequestRevokeIntegrationToken; import im.actor.core.api.rpc.RequestRevokeInviteUrl; +import im.actor.core.api.rpc.RequestSaveAdminSettings; +import im.actor.core.api.rpc.RequestShareHistory; +import im.actor.core.api.rpc.RequestTransferOwnership; import im.actor.core.api.rpc.ResponseIntegrationToken; import im.actor.core.api.rpc.ResponseInviteUrl; -import im.actor.core.api.updates.UpdateContactsAdded; -import im.actor.core.api.updates.UpdateGroupAboutChanged; -import im.actor.core.api.updates.UpdateGroupAboutChangedObsolete; -import im.actor.core.api.updates.UpdateGroupMembersUpdate; -import im.actor.core.api.updates.UpdateGroupMembersUpdateObsolete; -import im.actor.core.api.updates.UpdateGroupTitleChanged; -import im.actor.core.api.updates.UpdateGroupTitleChangedObsolete; -import im.actor.core.api.updates.UpdateGroupTopicChanged; -import im.actor.core.api.updates.UpdateGroupTopicChangedObsolete; -import im.actor.core.api.updates.UpdateGroupUserInvited; -import im.actor.core.api.updates.UpdateGroupUserInvitedObsolete; -import im.actor.core.api.updates.UpdateGroupUserKick; -import im.actor.core.api.updates.UpdateGroupUserKickObsolete; -import im.actor.core.api.updates.UpdateGroupUserLeave; -import im.actor.core.api.updates.UpdateGroupUserLeaveObsolete; -import im.actor.core.api.updates.UpdateMessage; +import im.actor.core.api.rpc.ResponseVoid; import im.actor.core.entity.Group; +import im.actor.core.entity.GroupMember; +import im.actor.core.entity.GroupMembersSlice; +import im.actor.core.entity.GroupPermissions; +import im.actor.core.entity.Peer; +import im.actor.core.entity.PeerType; import im.actor.core.entity.User; +import im.actor.core.events.PeerChatOpened; +import im.actor.core.events.PeerInfoOpened; import im.actor.core.modules.AbsModule; import im.actor.core.modules.ModuleContext; import im.actor.core.modules.api.ApiSupportConfiguration; @@ -63,6 +61,8 @@ import im.actor.runtime.Storage; import im.actor.runtime.actors.ActorRef; import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.eventbus.BusSubscriber; +import im.actor.runtime.eventbus.Event; import im.actor.runtime.function.Function; import im.actor.runtime.mvvm.MVVMCollection; import im.actor.runtime.promise.Promise; @@ -71,7 +71,11 @@ import static im.actor.runtime.actors.ActorSystem.system; -public class GroupsModule extends AbsModule { +public class GroupsModule extends AbsModule implements BusSubscriber { + + // Workaround for j2objc bug + private static final Void DUMB = null; + private static final ResponseVoid DUMB2 = null; private final KeyValueEngine groups; private final MVVMCollection collection; @@ -82,7 +86,7 @@ public class GroupsModule extends AbsModule { public GroupsModule(final ModuleContext context) { super(context); - collection = Storage.createKeyValue(STORAGE_GROUPS, GroupVM.CREATOR(context.getAuthModule().myUid()), Group.CREATOR); + collection = Storage.createKeyValue(STORAGE_GROUPS, GroupVM.CREATOR, Group.CREATOR); groups = collection.getEngine(); groupRouterInt = new GroupRouterInt(context); @@ -91,6 +95,10 @@ public GroupsModule(final ModuleContext context) { avatarChangeActor = system().actorOf("actor/avatar/group", () -> new GroupAvatarChangeActor(context)); } + public void run() { + context().getEvents().subscribe(this, PeerChatOpened.EVENT); + context().getEvents().subscribe(this, PeerInfoOpened.EVENT); + } // // Storage @@ -121,7 +129,17 @@ public GroupRouterInt getRouter() { // Actions // - public Promise createGroup(final String title, final String avatarDescriptor, final int[] uids) { + public Promise createGroup(String title, String avatarDescriptor, int[] uids) { + return createGroup(title, avatarDescriptor, uids, ApiGroupType.GROUP); + } + + public Promise createChannel(String title, String avatarDescriptor) { + return createGroup(title, avatarDescriptor, new int[0], ApiGroupType.CHANNEL); + } + + private Promise createGroup(String title, String avatarDescriptor, int[] uids, + ApiGroupType groupType) { + long rid = RandomUtils.nextRid(); return Promise.success(uids) .map((Function>) ints -> { ArrayList peers = new ArrayList<>(); @@ -134,9 +152,11 @@ public Promise createGroup(final String title, final String avatarDescr return peers; }) .flatMap(apiUserOutPeers -> - api(new RequestCreateGroup(RandomUtils.nextRid(), title, apiUserOutPeers, null, ApiSupportConfiguration.OPTIMIZATIONS))) - .chain(response -> updates().applyRelatedData(response.getUsers(), response.getGroup())) - .map(responseCreateGroup -> responseCreateGroup.getGroup().getId()) + api(new RequestCreateGroup(rid, title, apiUserOutPeers, + groupType, ApiSupportConfiguration.OPTIMIZATIONS))) + .chain(r -> updates().applyRelatedData(r.getUsers(), r.getGroup())) + .chain(r -> updates().waitForUpdate(r.getSeq())) + .map(r -> r.getGroup().getId()) .then(integer -> { if (avatarDescriptor != null) { changeAvatar(integer, avatarDescriptor); @@ -153,18 +173,10 @@ public Promise addMember(final int gid, final int uid) { rid, new ApiUserOutPeer(uid, groupUserTuple2.getT2().getAccessHash()), ApiSupportConfiguration.OPTIMIZATIONS))) - .flatMap(responseSeqDate -> - updates().applyUpdate( - responseSeqDate.getSeq(), - responseSeqDate.getState(), - new UpdateGroupUserInvitedObsolete( - gid, rid, - uid, myUid(), - responseSeqDate.getDate()) - )); - } - - public Promise kickMember(final int gid, final int uid) { + .flatMap(r -> updates().waitForUpdate(r.getSeq())); + } + + public Promise kickMember(int gid, int uid) { final long rid = RandomUtils.nextRid(); return Promises.tuple(getGroups().getValueAsync(gid), users().getValueAsync(uid)) .flatMap(groupUserTuple2 -> @@ -173,20 +185,10 @@ public Promise kickMember(final int gid, final int uid) { rid, new ApiUserOutPeer(uid, groupUserTuple2.getT2().getAccessHash()), ApiSupportConfiguration.OPTIMIZATIONS))) - .flatMap(responseSeqDate -> - updates().applyUpdate( - responseSeqDate.getSeq(), - responseSeqDate.getState(), - new UpdateGroupUserKickObsolete( - gid, - rid, - uid, - myUid(), - responseSeqDate.getDate()) - )); - } - - public Promise leaveGroup(final int gid) { + .flatMap(r -> updates().waitForUpdate(r.getSeq())); + } + + public Promise leaveGroup(int gid) { final long rid = RandomUtils.nextRid(); return getGroups().getValueAsync(gid) .flatMap(group -> @@ -194,32 +196,54 @@ public Promise leaveGroup(final int gid) { new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()), rid, ApiSupportConfiguration.OPTIMIZATIONS))) - .flatMap(responseSeqDate -> - updates().applyUpdate( - responseSeqDate.getSeq(), - responseSeqDate.getState(), - new UpdateGroupUserLeaveObsolete( - gid, - rid, - myUid(), - responseSeqDate.getDate()) - )); + .flatMap(r -> updates().waitForUpdate(r.getSeq())); } + public Promise leaveAndDeleteGroup(int gid) { + return getGroups().getValueAsync(gid) + .flatMap(group -> + api(new RequestLeaveAndDelete( + new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash())))) + .flatMap(r -> updates().waitForUpdate(r.getSeq())); + } + + public Promise deleteGroup(int gid) { + return getGroups().getValueAsync(gid) + .flatMap(group -> + api(new RequestDeleteGroup(new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash())))) + .flatMap(r -> updates().waitForUpdate(r.getSeq())); + } + + public Promise shareHistory(int gid) { + return getGroups().getValueAsync(gid) + .flatMap(group -> + api(new RequestShareHistory(new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash())))) + .flatMap(r -> updates().waitForUpdate(r.getSeq())); + } public Promise makeAdmin(final int gid, final int uid) { return Promises.tuple(getGroups().getValueAsync(gid), users().getValueAsync(uid)) .flatMap(groupUserTuple2 -> api(new RequestMakeUserAdminObsolete( new ApiGroupOutPeer(gid, groupUserTuple2.getT1().getAccessHash()), new ApiUserOutPeer(uid, groupUserTuple2.getT2().getAccessHash())))) - .flatMap(responseMakeUserAdmin -> - updates().applyUpdate( - responseMakeUserAdmin.getSeq(), - responseMakeUserAdmin.getState(), - new UpdateGroupMembersUpdateObsolete(gid, responseMakeUserAdmin.getMembers()) - )); + .flatMap(r -> updates().waitForUpdate(r.getSeq())); } + public Promise revokeAdmin(final int gid, final int uid) { + return Promises.tuple(getGroups().getValueAsync(gid), users().getValueAsync(uid)) + .flatMap(groupUserTuple2 -> api(new RequestDismissUserAdmin( + new ApiGroupOutPeer(gid, groupUserTuple2.getT1().getAccessHash()), + new ApiUserOutPeer(uid, groupUserTuple2.getT2().getAccessHash())))) + .flatMap(r -> updates().waitForUpdate(r.getSeq())); + } + + public Promise transferOwnership(final int gid, final int uid) { + return Promises.tuple(getGroups().getValueAsync(gid), users().getValueAsync(uid)) + .flatMap(groupUserTuple2 -> api(new RequestTransferOwnership( + new ApiGroupOutPeer(gid, groupUserTuple2.getT1().getAccessHash()), + new ApiUserOutPeer(uid, groupUserTuple2.getT2().getAccessHash())))) + .flatMap(r -> updates().waitForUpdate(r.getSeq())); + } public Promise editTitle(final int gid, final String name) { final long rid = RandomUtils.nextRid(); @@ -229,14 +253,7 @@ public Promise editTitle(final int gid, final String name) { new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()), rid, name, ApiSupportConfiguration.OPTIMIZATIONS))) - .flatMap(responseSeqDate -> updates().applyUpdate( - responseSeqDate.getSeq(), - responseSeqDate.getState(), - new UpdateGroupTitleChangedObsolete( - gid, rid, - myUid(), name, - responseSeqDate.getDate())) - ); + .flatMap(r -> updates().waitForUpdate(r.getSeq())); } public Promise editTheme(final int gid, final String theme) { @@ -247,14 +264,7 @@ public Promise editTheme(final int gid, final String theme) { new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()), rid, theme, ApiSupportConfiguration.OPTIMIZATIONS))) - .flatMap(responseSeqDate -> updates().applyUpdate( - responseSeqDate.getSeq(), - responseSeqDate.getState(), - new UpdateGroupTopicChangedObsolete( - gid, rid, - myUid(), theme, - responseSeqDate.getDate()) - )); + .flatMap(r -> updates().waitForUpdate(r.getSeq())); } public Promise editAbout(final int gid, final String about) { @@ -264,12 +274,47 @@ public Promise editAbout(final int gid, final String about) { api(new RequestEditGroupAbout( new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()), rid, about, ApiSupportConfiguration.OPTIMIZATIONS))) - .flatMap(responseSeqDate -> updates().applyUpdate( - responseSeqDate.getSeq(), - responseSeqDate.getState(), - new UpdateGroupAboutChangedObsolete( - gid, about) - )); + .flatMap(r -> updates().waitForUpdate(r.getSeq())); + } + + public Promise editShortName(final int gid, final String shortName) { + return getGroups().getValueAsync(gid) + .flatMap(group -> + api(new RequestEditGroupShortName(new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()), + shortName))) + .flatMap(r -> updates().waitForUpdate(r.getSeq())); + } + + public Promise loadAdminSettings(int gid) { + return getGroups().getValueAsync(gid) + .flatMap(group -> api(new RequestLoadAdminSettings(new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash())))) + .map(r -> new GroupPermissions(r.getSettings())); + } + + public Promise saveAdminSettings(int gid, GroupPermissions adminSettings) { + return getGroups().getValueAsync(gid) + .flatMap(group -> api(new RequestSaveAdminSettings( + new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()), + adminSettings.getApiSettings()))) + .map(r -> null); + } + + public Promise loadMembers(int gid, int limit, byte[] next) { + return getGroups().getValueAsync(gid) + .flatMap(group -> + api(new RequestLoadMembers( + new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash()), + limit, next))) + .chain(r -> updates().loadRequiredPeers(r.getUsers(), new ArrayList<>())) + .map(r -> { + ArrayList members = new ArrayList<>(); + for (ApiMember p : r.getMembers()) { + boolean isAdmin = p.isAdmin() != null ? p.isAdmin() : false; + members.add(new GroupMember(p.getUid(), + p.getInviterUid(), p.getInviterUid(), isAdmin)); + } + return new GroupMembersSlice(members, r.getNext()); + }); } public void changeAvatar(int gid, String descriptor) { @@ -304,26 +349,18 @@ public Promise requestRevokeLink(final int gid) { public Promise joinGroupByToken(final String token) { return api(new RequestJoinGroup(token, ApiSupportConfiguration.OPTIMIZATIONS)) .chain(responseJoinGroup -> updates().loadRequiredPeers(responseJoinGroup.getUserPeers(), new ArrayList<>())) - .chain(responseJoinGroup -> - updates().applyUpdate( - responseJoinGroup.getSeq(), - responseJoinGroup.getState(), - new UpdateMessage( - new ApiPeer(ApiPeerType.GROUP, responseJoinGroup.getGroup().getId()), - myUid(), - responseJoinGroup.getDate(), - responseJoinGroup.getRid(), - new ApiServiceMessage("Joined chat", - new ApiServiceExUserJoined()), - new ApiMessageAttributes(), - null), - responseJoinGroup.getUsers(), - responseJoinGroup.getGroup() - ) - ) + .chain(r -> updates().waitForUpdate(r.getSeq())) .map(responseJoinGroup -> responseJoinGroup.getGroup().getId()); } + public Promise joinGroup(int gid) { + return getGroups().getValueAsync(gid) + .flatMap(group -> + api(new RequestJoinGroupByPeer( + new ApiGroupOutPeer(group.getGroupId(), group.getAccessHash())))) + .chain(r -> updates().waitForUpdate(r.getSeq())) + .map(r -> null); + } // // Integration Token @@ -355,4 +392,19 @@ public Promise revokeIntegrationToken(final int gid) { public void resetModule() { groups.clear(); } + + @Override + public void onBusEvent(Event event) { + if (event instanceof PeerChatOpened) { + Peer peer = ((PeerChatOpened) event).getPeer(); + if (peer.getPeerType() == PeerType.GROUP) { + getRouter().onFullGroupNeeded(peer.getPeerId()); + } + } else if (event instanceof PeerInfoOpened) { + Peer peer = ((PeerInfoOpened) event).getPeer(); + if (peer.getPeerType() == PeerType.GROUP) { + getRouter().onFullGroupNeeded(peer.getPeerId()); + } + } + } } \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/GroupsProcessor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/GroupsProcessor.java index 22d244e409..66872e6c15 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/GroupsProcessor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/GroupsProcessor.java @@ -5,23 +5,23 @@ package im.actor.core.modules.groups; import im.actor.core.api.updates.UpdateGroupAboutChanged; -import im.actor.core.api.updates.UpdateGroupAboutChangedObsolete; import im.actor.core.api.updates.UpdateGroupAvatarChanged; -import im.actor.core.api.updates.UpdateGroupAvatarChangedObsolete; -import im.actor.core.api.updates.UpdateGroupInvite; -import im.actor.core.api.updates.UpdateGroupInviteObsolete; -import im.actor.core.api.updates.UpdateGroupMembersUpdate; -import im.actor.core.api.updates.UpdateGroupMembersUpdateObsolete; +import im.actor.core.api.updates.UpdateGroupDeleted; +import im.actor.core.api.updates.UpdateGroupExtChanged; +import im.actor.core.api.updates.UpdateGroupFullExtChanged; +import im.actor.core.api.updates.UpdateGroupFullPermissionsChanged; +import im.actor.core.api.updates.UpdateGroupHistoryShared; +import im.actor.core.api.updates.UpdateGroupMemberAdminChanged; +import im.actor.core.api.updates.UpdateGroupMemberChanged; +import im.actor.core.api.updates.UpdateGroupMemberDiff; +import im.actor.core.api.updates.UpdateGroupMembersBecameAsync; +import im.actor.core.api.updates.UpdateGroupMembersCountChanged; +import im.actor.core.api.updates.UpdateGroupMembersUpdated; +import im.actor.core.api.updates.UpdateGroupOwnerChanged; +import im.actor.core.api.updates.UpdateGroupPermissionsChanged; +import im.actor.core.api.updates.UpdateGroupShortNameChanged; import im.actor.core.api.updates.UpdateGroupTitleChanged; -import im.actor.core.api.updates.UpdateGroupTitleChangedObsolete; import im.actor.core.api.updates.UpdateGroupTopicChanged; -import im.actor.core.api.updates.UpdateGroupTopicChangedObsolete; -import im.actor.core.api.updates.UpdateGroupUserInvited; -import im.actor.core.api.updates.UpdateGroupUserInvitedObsolete; -import im.actor.core.api.updates.UpdateGroupUserKick; -import im.actor.core.api.updates.UpdateGroupUserKickObsolete; -import im.actor.core.api.updates.UpdateGroupUserLeave; -import im.actor.core.api.updates.UpdateGroupUserLeaveObsolete; import im.actor.core.modules.AbsModule; import im.actor.core.modules.ModuleContext; import im.actor.core.modules.sequence.processor.SequenceProcessor; @@ -37,15 +37,26 @@ public GroupsProcessor(ModuleContext context) { @Override public Promise process(Update update) { - if (update instanceof UpdateGroupTitleChangedObsolete || - update instanceof UpdateGroupTopicChangedObsolete || - update instanceof UpdateGroupAboutChangedObsolete || - update instanceof UpdateGroupAvatarChangedObsolete || - update instanceof UpdateGroupInviteObsolete || - update instanceof UpdateGroupUserLeaveObsolete || - update instanceof UpdateGroupUserKickObsolete || - update instanceof UpdateGroupUserInvitedObsolete || - update instanceof UpdateGroupMembersUpdateObsolete) { + if (update instanceof UpdateGroupTitleChanged || + update instanceof UpdateGroupMemberChanged || + update instanceof UpdateGroupAvatarChanged || + update instanceof UpdateGroupPermissionsChanged || + update instanceof UpdateGroupDeleted || + update instanceof UpdateGroupExtChanged || + + update instanceof UpdateGroupMembersUpdated || + update instanceof UpdateGroupMemberAdminChanged || + update instanceof UpdateGroupMemberDiff || + update instanceof UpdateGroupMembersBecameAsync || + update instanceof UpdateGroupMembersCountChanged || + + update instanceof UpdateGroupShortNameChanged || + update instanceof UpdateGroupAboutChanged || + update instanceof UpdateGroupTopicChanged || + update instanceof UpdateGroupOwnerChanged || + update instanceof UpdateGroupHistoryShared || + update instanceof UpdateGroupFullPermissionsChanged || + update instanceof UpdateGroupFullExtChanged) { return context().getGroupsModule().getRouter().onUpdate(update); } return null; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/router/GroupRouter.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/router/GroupRouter.java index 0c7cac97eb..e2291fbea0 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/router/GroupRouter.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/router/GroupRouter.java @@ -3,47 +3,40 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import im.actor.core.api.ApiAvatar; import im.actor.core.api.ApiGroup; import im.actor.core.api.ApiGroupOutPeer; +import im.actor.core.api.ApiMapValue; import im.actor.core.api.ApiMember; -import im.actor.core.api.ApiUser; -import im.actor.core.api.ApiUserOutPeer; +import im.actor.core.api.rpc.RequestLoadFullGroups; import im.actor.core.api.updates.UpdateGroupAboutChanged; -import im.actor.core.api.updates.UpdateGroupAboutChangedObsolete; import im.actor.core.api.updates.UpdateGroupAvatarChanged; -import im.actor.core.api.updates.UpdateGroupAvatarChangedObsolete; -import im.actor.core.api.updates.UpdateGroupInvite; -import im.actor.core.api.updates.UpdateGroupInviteObsolete; -import im.actor.core.api.updates.UpdateGroupMembersUpdate; -import im.actor.core.api.updates.UpdateGroupMembersUpdateObsolete; +import im.actor.core.api.updates.UpdateGroupDeleted; +import im.actor.core.api.updates.UpdateGroupExtChanged; +import im.actor.core.api.updates.UpdateGroupFullExtChanged; +import im.actor.core.api.updates.UpdateGroupFullPermissionsChanged; +import im.actor.core.api.updates.UpdateGroupHistoryShared; +import im.actor.core.api.updates.UpdateGroupMemberAdminChanged; +import im.actor.core.api.updates.UpdateGroupMemberChanged; +import im.actor.core.api.updates.UpdateGroupMemberDiff; +import im.actor.core.api.updates.UpdateGroupMembersBecameAsync; +import im.actor.core.api.updates.UpdateGroupMembersCountChanged; +import im.actor.core.api.updates.UpdateGroupMembersUpdated; +import im.actor.core.api.updates.UpdateGroupOwnerChanged; +import im.actor.core.api.updates.UpdateGroupPermissionsChanged; +import im.actor.core.api.updates.UpdateGroupShortNameChanged; import im.actor.core.api.updates.UpdateGroupTitleChanged; -import im.actor.core.api.updates.UpdateGroupTitleChangedObsolete; import im.actor.core.api.updates.UpdateGroupTopicChanged; -import im.actor.core.api.updates.UpdateGroupTopicChangedObsolete; -import im.actor.core.api.updates.UpdateGroupUserInvited; -import im.actor.core.api.updates.UpdateGroupUserInvitedObsolete; -import im.actor.core.api.updates.UpdateGroupUserKick; -import im.actor.core.api.updates.UpdateGroupUserKickObsolete; -import im.actor.core.api.updates.UpdateGroupUserLeave; -import im.actor.core.api.updates.UpdateGroupUserLeaveObsolete; import im.actor.core.entity.Group; -import im.actor.core.entity.Message; -import im.actor.core.entity.MessageState; -import im.actor.core.entity.User; -import im.actor.core.entity.content.ServiceGroupAvatarChanged; -import im.actor.core.entity.content.ServiceGroupCreated; -import im.actor.core.entity.content.ServiceGroupTitleChanged; -import im.actor.core.entity.content.ServiceGroupUserInvited; -import im.actor.core.entity.content.ServiceGroupUserKicked; -import im.actor.core.entity.content.ServiceGroupUserLeave; import im.actor.core.modules.ModuleActor; import im.actor.core.modules.ModuleContext; import im.actor.core.modules.groups.router.entity.RouterApplyGroups; import im.actor.core.modules.groups.router.entity.RouterFetchMissingGroups; import im.actor.core.modules.groups.router.entity.RouterGroupUpdate; +import im.actor.core.modules.groups.router.entity.RouterLoadFullGroup; import im.actor.core.modules.messaging.router.RouterInt; import im.actor.core.network.parser.Update; import im.actor.runtime.actors.messages.Void; @@ -58,200 +51,122 @@ public class GroupRouter extends ModuleActor { // j2objc workaround private static final Void DUMB = null; + private final HashSet requestedFullGroups = new HashSet<>(); private boolean isFreezed = false; public GroupRouter(ModuleContext context) { super(context); } - // - // Updates + // Updates Main // @Verified - public Promise onGroupInvite(int groupId, long rid, int inviterId, long date, boolean isSilent) { - return forGroup(groupId, group -> { - - groups().addOrUpdateItem(group - .addMember(myUid(), inviterId, date)); - - if (!isSilent) { - if (inviterId == myUid()) { - // If current user invite himself, add create group message - Message message = new Message(rid, date, date, inviterId, - MessageState.UNKNOWN, ServiceGroupCreated.create()); - return getRouter().onNewMessage(group.peer(), message); - } else { - // else add invite message - Message message = new Message(rid, date, date, inviterId, - MessageState.SENT, ServiceGroupUserInvited.create(myUid())); - return getRouter().onNewMessage(group.peer(), message); - } - } - - return Promise.success(null); - }); + public Promise onAvatarChanged(int groupId, @Nullable ApiAvatar avatar) { + return editDescGroup(groupId, group -> group.editAvatar(avatar)); } @Verified - public Promise onUserLeave(int groupId, long rid, int uid, long date, boolean isSilent) { - return forGroup(groupId, group -> { - - if (uid == myUid()) { - // If current user leave, clear members and change member state - groups().addOrUpdateItem(group - .clearMembers()); - } else { - // else remove leaved user - groups().addOrUpdateItem(group - .removeMember(uid)); - } - - // Create message if needed - if (!isSilent) { - Message message = new Message(rid, date, date, uid, - uid == myUid() ? MessageState.SENT : MessageState.UNKNOWN, - ServiceGroupUserLeave.create()); - return getRouter().onNewMessage(group.peer(), message); - } - - return Promise.success(null); - }); + public Promise onTitleChanged(int groupId, String title) { + return editDescGroup(groupId, group -> group.editTitle(title)); } @Verified - public Promise onUserKicked(int groupId, long rid, int uid, int kicker, long date, boolean isSilent) { - return forGroup(groupId, group -> { - - if (uid == myUid()) { - // If kicked me, clear members and change member state - groups().addOrUpdateItem(group - .clearMembers()); - } else { - // else remove kicked user - groups().addOrUpdateItem(group - .removeMember(uid)); - } - - // Create message if needed - if (!isSilent) { - Message message = new Message(rid, date, date, kicker, - kicker == myUid() ? MessageState.SENT : MessageState.UNKNOWN, - ServiceGroupUserKicked.create(uid)); - return getRouter().onNewMessage(group.peer(), message); - } - - return Promise.success(null); - }); + public Promise onIsMemberChanged(int groupId, boolean isMember) { + return editGroup(groupId, group -> group.editIsMember(isMember)); } @Verified - public Promise onUserAdded(int groupId, long rid, int uid, int adder, long date, boolean isSilent) { - return forGroup(groupId, group -> { - - groups().addOrUpdateItem(group.addMember(uid, adder, date)); + public Promise onPermissionsChanged(int groupId, long permissions) { + return editGroup(groupId, group -> group.editPermissions(permissions)); + } - // Create message if needed - if (!isSilent) { - Message message = new Message(rid, date, date, adder, - adder == myUid() ? MessageState.SENT : MessageState.UNKNOWN, - ServiceGroupUserInvited.create(uid)); - return getRouter().onNewMessage(group.peer(), message); - } - return Promise.success(null); - }); + @Verified + public Promise onGroupDeleted(int groupId) { + return editGroup(groupId, group -> group.editIsDeleted(true)); } @Verified - public Promise onTitleChanged(int groupId, long rid, int uid, String title, long date, - boolean isSilent) { - return forGroup(groupId, group -> { + public Promise onExtChanged(int groupId, ApiMapValue ext) { + return editGroup(groupId, group -> group.editExt(ext)); + } - // Change group title - Group upd = group.editTitle(title); + // + // Members Updates + // - // Update group - groups().addOrUpdateItem(upd); + @Verified + public Promise onMembersChanged(int groupId, List members) { + return editGroup(groupId, group -> group.editMembers(members)); + } - // Notify about group change - Promise src = onGroupDescChanged(upd); + @Verified + public Promise onMembersChanged(int groupId, List added, List removed, int count) { + return editGroup(groupId, group -> group.editMembers(added, removed, count)); + } - // Create message if needed - if (!isSilent) { - Message message = new Message(rid, date, date, uid, - uid == myUid() ? MessageState.SENT : MessageState.UNKNOWN, - ServiceGroupTitleChanged.create(title)); - src = src.chain(v -> getRouter().onNewMessage(group.peer(), message)); - } - return Promise.success(null); - }); + @Verified + public Promise onMembersChanged(int groupId, int membersCount) { + return editGroup(groupId, group -> group.editMembersCount(membersCount)); } @Verified - public Promise onTopicChanged(int groupId, String topic) { - return forGroup(groupId, group -> { + public Promise onMembersBecameAsync(int groupId) { + return editGroup(groupId, group -> group.editMembersBecameAsync()); + } - // Change group title - Group upd = group.editTheme(topic); + @Verified + public Promise onMemberChangedAdmin(int groupId, int uid, Boolean isAdmin) { + return editGroup(groupId, group -> group.editMemberChangedAdmin(uid, isAdmin)); + } - // Update group - groups().addOrUpdateItem(upd); + // + // Updates Ext + // - // Notify about group change - return onGroupDescChanged(upd); - }); + @Verified + public Promise onTopicChanged(int groupId, String topic) { + return editGroup(groupId, group -> group.editTopic(topic)); } @Verified public Promise onAboutChanged(int groupId, String about) { - return forGroup(groupId, group -> { - - groups().addOrUpdateItem(group.editAbout(about)); - - return Promise.success(null); - }); + return editGroup(groupId, group -> group.editAbout(about)); } @Verified - public Promise onAvatarChanged(int groupId, long rid, int uid, @Nullable ApiAvatar avatar, long date, - boolean isSilent) { - - return forGroup(groupId, group -> { - - // Change group avatar - Group upd = group.editAvatar(avatar); - - // Update group - groups().addOrUpdateItem(upd); + public Promise onShortNameChanged(int groupId, String shortName) { + return editGroup(groupId, group -> group.editShortName(shortName)); + } - // Notify about group change - Promise src = onGroupDescChanged(upd); - // Create message if needed - if (!isSilent) { - Message message = new Message(rid, date, date, uid, - uid == myUid() ? MessageState.SENT : MessageState.UNKNOWN, - ServiceGroupAvatarChanged.create(avatar)); - src.chain(v -> getRouter().onNewMessage(group.peer(), message)); - } - return src; - }); + @Verified + public Promise onOwnerChanged(int groupId, int updatedOwner) { + return editGroup(groupId, group -> group.editOwner(updatedOwner)); } - @Verified - public Promise onMembersUpdated(int groupId, List members) { - return forGroup(groupId, group -> { + public Promise onHistoryShared(int groupId) { + return editGroup(groupId, group -> group.editHistoryShared()); + } - groups().addOrUpdateItem(group.updateMembers(members)); + @Verified + public Promise onFullPermissionsChanged(int groupId, long permissions) { + return editGroup(groupId, group -> group.editExtPermissions(permissions)); + } - return Promise.success(null); - }); + @Verified + public Promise onFullExtChanged(int groupId, ApiMapValue ext) { + return editGroup(groupId, group -> group.editFullExt(ext)); } + // + // Wrapper + // + private Promise forGroup(int groupId, Function> func) { - isFreezed = true; + freeze(); return groups().getValueAsync(groupId) .fallback(e -> null) .flatMap(g -> { @@ -260,12 +175,25 @@ private Promise forGroup(int groupId, Function> func) } return Promise.success(null); }) - .then(v -> { - isFreezed = false; - unstashAll(); + .after((v, e) -> { + unfreeze(); }); } + private Promise editGroup(int groupId, Function func) { + return forGroup(groupId, group -> { + groups().addOrUpdateItem(func.apply(group)); + return Promise.success(null); + }); + } + + private Promise editDescGroup(int groupId, Function func) { + return forGroup(groupId, group -> { + Group g = func.apply(group); + groups().addOrUpdateItem(g); + return onGroupDescChanged(g); + }); + } // // Entities @@ -273,21 +201,20 @@ private Promise forGroup(int groupId, Function> func) @Verified private Promise> fetchMissingGroups(List groups) { - isFreezed = true; + freeze(); return PromisesArray.of(groups) .map((Function>) u -> groups().containsAsync(u.getGroupId()) .map(v -> v ? null : u)) .filterNull() .zip() .after((r, e) -> { - isFreezed = false; - unstashAll(); + unfreeze(); }); } @Verified private Promise applyGroups(List groups) { - isFreezed = true; + freeze(); return PromisesArray.of(groups) .map((Function>>) u -> groups().containsAsync(u.getId()) .map(v -> new Tuple2<>(u, v))) @@ -296,19 +223,41 @@ private Promise applyGroups(List groups) { .then(x -> { List res = new ArrayList<>(); for (Tuple2 u : x) { - res.add(new Group(u.getT1())); + res.add(new Group(u.getT1(), null)); } if (res.size() > 0) { groups().addOrUpdateItems(res); } }) .map(x -> (Void) null) - .after((r, e) -> { - isFreezed = false; - unstashAll(); - }); + .after((r, e) -> unfreeze()); } + private void onRequestLoadFullGroup(int gid) { + if (requestedFullGroups.contains(gid)) { + return; + } + requestedFullGroups.add(gid); + + freeze(); + groups().getValueAsync(gid) + // Do not reduce to lambda due j2objc bug + .flatMap(new Function>() { + @Override + public Promise apply(Group group) { + if (!group.isHaveExtension()) { + ArrayList groups = new ArrayList<>(); + groups.add(new ApiGroupOutPeer(gid, group.getAccessHash())); + return api(new RequestLoadFullGroups(groups)) + .map(r -> group.updateExt(r.getGroups().get(0))); + } else { + return Promise.failure(new RuntimeException("Already loaded")); + } + } + }) + .then(r -> groups().addOrUpdateItem(r)) + .after((r, e) -> unfreeze()); + } // // Tools @@ -323,51 +272,96 @@ private RouterInt getRouter() { return context().getMessagesModule().getRouter(); } + private void freeze() { + isFreezed = true; + } + + private void unfreeze() { + isFreezed = false; + unstashAll(); + } + // // Messages // private Promise onUpdate(Update update) { - if (update instanceof UpdateGroupTitleChangedObsolete) { - UpdateGroupTitleChangedObsolete titleChanged = (UpdateGroupTitleChangedObsolete) update; - return onTitleChanged(titleChanged.getGroupId(), titleChanged.getRid(), - titleChanged.getUid(), titleChanged.getTitle(), titleChanged.getDate(), - false); - } else if (update instanceof UpdateGroupTopicChangedObsolete) { - UpdateGroupTopicChangedObsolete topicChanged = (UpdateGroupTopicChangedObsolete) update; + + // + // Main + // + + if (update instanceof UpdateGroupTitleChanged) { + UpdateGroupTitleChanged titleChanged = (UpdateGroupTitleChanged) update; + return onTitleChanged(titleChanged.getGroupId(), titleChanged.getTitle()); + } else if (update instanceof UpdateGroupAvatarChanged) { + UpdateGroupAvatarChanged avatarChanged = (UpdateGroupAvatarChanged) update; + return onAvatarChanged(avatarChanged.getGroupId(), avatarChanged.getAvatar()); + } else if (update instanceof UpdateGroupMemberChanged) { + UpdateGroupMemberChanged memberChanged = (UpdateGroupMemberChanged) update; + return onIsMemberChanged(memberChanged.getGroupId(), memberChanged.isMember()); + } else if (update instanceof UpdateGroupPermissionsChanged) { + UpdateGroupPermissionsChanged permissionsChanged = (UpdateGroupPermissionsChanged) update; + return onPermissionsChanged(permissionsChanged.getGroupId(), permissionsChanged.getPermissions()); + } else if (update instanceof UpdateGroupDeleted) { + UpdateGroupDeleted groupDeleted = (UpdateGroupDeleted) update; + return onGroupDeleted(groupDeleted.getGroupId()); + } else if (update instanceof UpdateGroupExtChanged) { + UpdateGroupExtChanged extChanged = (UpdateGroupExtChanged) update; + return onExtChanged(extChanged.getGroupId(), extChanged.getExt()); + } + + // + // Members + // + + else if (update instanceof UpdateGroupMembersUpdated) { + UpdateGroupMembersUpdated membersUpdate = (UpdateGroupMembersUpdated) update; + return onMembersChanged(membersUpdate.getGroupId(), membersUpdate.getMembers()); + } else if (update instanceof UpdateGroupMemberAdminChanged) { + UpdateGroupMemberAdminChanged adminChanged = (UpdateGroupMemberAdminChanged) update; + return onMemberChangedAdmin(adminChanged.getGroupId(), adminChanged.getUserId(), + adminChanged.isAdmin()); + } else if (update instanceof UpdateGroupMemberDiff) { + UpdateGroupMemberDiff memberDiff = (UpdateGroupMemberDiff) update; + return onMembersChanged(memberDiff.getGroupId(), memberDiff.getAddedMembers(), + memberDiff.getRemovedUsers(), memberDiff.getMembersCount()); + } else if (update instanceof UpdateGroupMembersBecameAsync) { + UpdateGroupMembersBecameAsync becameAsync = (UpdateGroupMembersBecameAsync) update; + return onMembersBecameAsync(becameAsync.getGroupId()); + } else if (update instanceof UpdateGroupMembersCountChanged) { + UpdateGroupMembersCountChanged membersCountChanged = (UpdateGroupMembersCountChanged) update; + return onMembersChanged(membersCountChanged.getGroupId(), membersCountChanged.getMembersCount()); + } + + // + // Ext + // + + else if (update instanceof UpdateGroupTopicChanged) { + UpdateGroupTopicChanged topicChanged = (UpdateGroupTopicChanged) update; return onTopicChanged(topicChanged.getGroupId(), topicChanged.getTopic()); - } else if (update instanceof UpdateGroupAboutChangedObsolete) { - UpdateGroupAboutChangedObsolete aboutChanged = (UpdateGroupAboutChangedObsolete) update; + } else if (update instanceof UpdateGroupAboutChanged) { + UpdateGroupAboutChanged aboutChanged = (UpdateGroupAboutChanged) update; return onAboutChanged(aboutChanged.getGroupId(), aboutChanged.getAbout()); - } else if (update instanceof UpdateGroupAvatarChangedObsolete) { - UpdateGroupAvatarChangedObsolete avatarChanged = (UpdateGroupAvatarChangedObsolete) update; - return onAvatarChanged(avatarChanged.getGroupId(), avatarChanged.getRid(), - avatarChanged.getUid(), avatarChanged.getAvatar(), - avatarChanged.getDate(), false); - } else if (update instanceof UpdateGroupInviteObsolete) { - UpdateGroupInviteObsolete groupInvite = (UpdateGroupInviteObsolete) update; - return onGroupInvite(groupInvite.getGroupId(), - groupInvite.getRid(), groupInvite.getInviteUid(), groupInvite.getDate(), - false); - } else if (update instanceof UpdateGroupUserLeaveObsolete) { - UpdateGroupUserLeaveObsolete leave = (UpdateGroupUserLeaveObsolete) update; - return onUserLeave(leave.getGroupId(), leave.getRid(), leave.getUid(), - leave.getDate(), false); - } else if (update instanceof UpdateGroupUserKickObsolete) { - UpdateGroupUserKickObsolete userKick = (UpdateGroupUserKickObsolete) update; - return onUserKicked(userKick.getGroupId(), - userKick.getRid(), userKick.getUid(), userKick.getKickerUid(), userKick.getDate(), - false); - } else if (update instanceof UpdateGroupUserInvitedObsolete) { - UpdateGroupUserInvitedObsolete userInvited = (UpdateGroupUserInvitedObsolete) update; - return onUserAdded(userInvited.getGroupId(), - userInvited.getRid(), userInvited.getUid(), userInvited.getInviterUid(), userInvited.getDate(), - false); - } else if (update instanceof UpdateGroupMembersUpdateObsolete) { - return onMembersUpdated(((UpdateGroupMembersUpdateObsolete) update).getGroupId(), - ((UpdateGroupMembersUpdateObsolete) update).getMembers()); + } else if (update instanceof UpdateGroupHistoryShared) { + UpdateGroupHistoryShared historyShared = (UpdateGroupHistoryShared) update; + return onHistoryShared(historyShared.getGroupId()); + } else if (update instanceof UpdateGroupOwnerChanged) { + UpdateGroupOwnerChanged ownerChanged = (UpdateGroupOwnerChanged) update; + return onOwnerChanged(ownerChanged.getGroupId(), ownerChanged.getUserId()); + } else if (update instanceof UpdateGroupShortNameChanged) { + UpdateGroupShortNameChanged shortNameChanged = (UpdateGroupShortNameChanged) update; + return onShortNameChanged(shortNameChanged.getGroupId(), shortNameChanged.getShortName()); + } else if (update instanceof UpdateGroupFullPermissionsChanged) { + UpdateGroupFullPermissionsChanged permissionsChanged = (UpdateGroupFullPermissionsChanged) update; + return onFullPermissionsChanged(permissionsChanged.getGroupId(), permissionsChanged.getPermissions()); + } else if (update instanceof UpdateGroupFullExtChanged) { + UpdateGroupFullExtChanged extChanged = (UpdateGroupFullExtChanged) update; + return onFullExtChanged(extChanged.getGroupId(), extChanged.getExt()); } + return Promise.success(null); } @@ -395,4 +389,18 @@ public Promise onAsk(Object message) throws Exception { return super.onAsk(message); } } + + @Override + public void onReceive(Object message) { + + if (message instanceof RouterLoadFullGroup) { + if (isFreezed) { + stash(); + return; + } + onRequestLoadFullGroup(((RouterLoadFullGroup) message).getGid()); + } else { + super.onReceive(message); + } + } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/router/GroupRouterInt.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/router/GroupRouterInt.java index db24b390f4..a6f07e8a3e 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/router/GroupRouterInt.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/router/GroupRouterInt.java @@ -8,6 +8,7 @@ import im.actor.core.modules.groups.router.entity.RouterApplyGroups; import im.actor.core.modules.groups.router.entity.RouterFetchMissingGroups; import im.actor.core.modules.groups.router.entity.RouterGroupUpdate; +import im.actor.core.modules.groups.router.entity.RouterLoadFullGroup; import im.actor.core.network.parser.Update; import im.actor.runtime.actors.ActorInterface; import im.actor.runtime.actors.messages.Void; @@ -32,4 +33,8 @@ public Promise> fetchPendingGroups(List p public Promise onUpdate(Update update) { return ask(new RouterGroupUpdate(update)); } + + public void onFullGroupNeeded(int gid) { + send(new RouterLoadFullGroup(gid)); + } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/router/entity/RouterLoadFullGroup.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/router/entity/RouterLoadFullGroup.java new file mode 100644 index 0000000000..a0240e431a --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/groups/router/entity/RouterLoadFullGroup.java @@ -0,0 +1,14 @@ +package im.actor.core.modules.groups.router.entity; + +public class RouterLoadFullGroup { + + private int gid; + + public RouterLoadFullGroup(int gid) { + this.gid = gid; + } + + public int getGid() { + return gid; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/MessagesModule.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/MessagesModule.java index a7b49d824b..7cc463e32b 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/MessagesModule.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/MessagesModule.java @@ -56,6 +56,7 @@ import im.actor.core.modules.messaging.actions.CursorReaderActor; import im.actor.core.modules.messaging.actions.CursorReceiverActor; import im.actor.core.modules.messaging.dialogs.DialogsActor; +import im.actor.core.modules.messaging.history.ConversationHistory; import im.actor.core.modules.messaging.history.ConversationHistoryActor; import im.actor.core.modules.messaging.history.DialogsHistoryActor; import im.actor.core.modules.messaging.actions.MessageDeleteActor; @@ -105,7 +106,7 @@ public class MessagesModule extends AbsModule implements BusSubscriber { private ActorRef sendMessageActor; private ActorRef deletionsActor; private RouterInt router; - private final HashMap historyLoaderActors = new HashMap<>(); + private final HashMap historyLoaderActors = new HashMap<>(); private MVVMCollection conversationStates; @@ -159,12 +160,10 @@ public ActorRef getPlainReceiverActor() { return plainReceiverActor; } - public ActorRef getHistoryActor(final Peer peer) { + public ConversationHistory getHistoryActor(final Peer peer) { synchronized (historyLoaderActors) { if (!historyLoaderActors.containsKey(peer)) { - historyLoaderActors.put(peer, system().actorOf("history/" + peer, () -> { - return new ConversationHistoryActor(peer, context()); - })); + historyLoaderActors.put(peer, new ConversationHistory(peer, context())); } return historyLoaderActors.get(peer); } @@ -448,7 +447,7 @@ public void loadMoreArchivedDialogs(final boolean init, final RpcCallback getHistoryActor(peer).send(new ConversationHistoryActor.LoadMore())); + im.actor.runtime.Runtime.dispatch(() -> getHistoryActor(peer).loadMore()); } // diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/MessagesProcessor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/MessagesProcessor.java index 45536fd91f..0a400982a8 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/MessagesProcessor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/MessagesProcessor.java @@ -13,6 +13,7 @@ import im.actor.core.api.ApiPeer; import im.actor.core.api.updates.UpdateChatClear; import im.actor.core.api.updates.UpdateChatDelete; +import im.actor.core.api.updates.UpdateChatDropCache; import im.actor.core.api.updates.UpdateChatGroupsChanged; import im.actor.core.api.updates.UpdateMessage; import im.actor.core.api.updates.UpdateMessageContentChanged; @@ -97,6 +98,7 @@ public Promise process(Update update) { update instanceof UpdateMessageContentChanged || update instanceof UpdateChatClear || update instanceof UpdateChatDelete || + update instanceof UpdateChatDropCache || update instanceof UpdateChatGroupsChanged || update instanceof UpdateReactionsUpdate || update instanceof UpdateMessageSent) { diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/SenderActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/SenderActor.java index 098ef9dbf9..8deddc101d 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/SenderActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/actions/SenderActor.java @@ -150,17 +150,19 @@ public void doSendText(@NotNull Peer peer, @NotNull String text, if (peer.getPeerType() == PeerType.GROUP) { Group group = getGroup(peer.getPeerId()); String lowText = text.toLowerCase(); - for (GroupMember member : group.getMembers()) { - User user = getUser(member.getUid()); - if (user.getNick() != null) { - String nick = "@" + user.getNick().toLowerCase(); - // TODO: Better filtering - if (lowText.contains(nick + ":") - || lowText.contains(nick + " ") - || lowText.contains(" " + nick) - || lowText.endsWith(nick) - || lowText.equals(nick)) { - mentions.add(user.getUid()); + if (group.getMembers() != null) { + for (GroupMember member : group.getMembers()) { + User user = getUser(member.getUid()); + if (user.getNick() != null) { + String nick = "@" + user.getNick().toLowerCase(); + // TODO: Better filtering + if (lowText.contains(nick + ":") + || lowText.contains(nick + " ") + || lowText.contains(" " + nick) + || lowText.endsWith(nick) + || lowText.equals(nick)) { + mentions.add(user.getUid()); + } } } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/dialogs/DialogsActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/dialogs/DialogsActor.java index bde055b7ac..ef3e642582 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/dialogs/DialogsActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/dialogs/DialogsActor.java @@ -13,6 +13,7 @@ import im.actor.core.entity.Dialog; import im.actor.core.entity.DialogBuilder; import im.actor.core.entity.Group; +import im.actor.core.entity.GroupType; import im.actor.core.entity.Message; import im.actor.core.entity.Peer; import im.actor.core.entity.User; @@ -86,14 +87,18 @@ private Promise onMessage(Peer peer, Message message, boolean forceWrite, .setMessageType(contentDescription.getContentType()) .setText(contentDescription.getText()) .setRelatedUid(contentDescription.getRelatedUser()) - .setSenderId(message.getSenderId()); + .setSenderId(message.getSenderId()) + .setDialogTitle(peerDesc.getTitle()) + .setDialogAvatar(peerDesc.getAvatar()) + .setIsBot(peerDesc.isBot()) + .setIsChannel(peerDesc.isChannel()); if (counter >= 0) { builder.setUnreadCount(counter); } boolean forceUpdate = false; - + boolean needUpdateSearch = false; if (dialog != null) { // Ignore old messages if no force if (!forceWrite && dialog.getSortDate() > message.getSortDate()) { @@ -102,8 +107,6 @@ private Promise onMessage(Peer peer, Message message, boolean forceWrite, } builder.setPeer(dialog.getPeer()) - .setDialogTitle(dialog.getDialogTitle()) - .setDialogAvatar(dialog.getDialogAvatar()) .setSortKey(dialog.getSortDate()) .updateKnownReceiveDate(dialog.getKnownReceiveDate()) .updateKnownReadDate(dialog.getKnownReadDate()); @@ -121,14 +124,16 @@ private Promise onMessage(Peer peer, Message message, boolean forceWrite, } builder.setPeer(peer) - .setDialogTitle(peerDesc.getTitle()) - .setDialogAvatar(peerDesc.getAvatar()) .setSortKey(message.getSortDate()); - + needUpdateSearch = true; forceUpdate = true; } - addOrUpdateItem(builder.createDialog()); + Dialog dialog1 = builder.createDialog(); + addOrUpdateItem(dialog1); + if (needUpdateSearch) { + updateSearch(dialog1); + } notifyState(forceUpdate); } @@ -301,7 +306,6 @@ private Promise onHistoryLoaded(List history) { } addOrUpdateItems(updated); updateSearch(updated); - context().getAppStateModule().onDialogsLoaded(); notifyState(true); return Promise.success(null); } @@ -333,7 +337,7 @@ private void notifyState(boolean force) { if (!isEmpty.equals(emptyNotified)) { emptyNotified = isEmpty; - context().getAppStateModule().onDialogsUpdate(isEmpty); + context().getConductor().getConductor().onDialogsChanged(isEmpty); } } @@ -342,10 +346,10 @@ private PeerDesc buildPeerDesc(Peer peer) { switch (peer.getPeerType()) { case PRIVATE: User u = getUser(peer.getPeerId()); - return new PeerDesc(u.getName(), u.getAvatar()); + return new PeerDesc(u.getName(), u.getAvatar(), u.isBot(), false); case GROUP: Group g = getGroup(peer.getPeerId()); - return new PeerDesc(g.getTitle(), g.getAvatar()); + return new PeerDesc(g.getTitle(), g.getAvatar(), false, g.getGroupType() == GroupType.CHANNEL); default: return null; } @@ -355,10 +359,14 @@ private class PeerDesc { private String title; private Avatar avatar; + private boolean isBot; + private boolean isChannel; - private PeerDesc(String title, Avatar avatar) { + private PeerDesc(String title, Avatar avatar, boolean isBot, boolean isChannel) { this.title = title; this.avatar = avatar; + this.isBot = isBot; + this.isChannel = isChannel; } public String getTitle() { @@ -368,6 +376,14 @@ public String getTitle() { public Avatar getAvatar() { return avatar; } + + public boolean isBot() { + return isBot; + } + + public boolean isChannel() { + return isChannel; + } } // Messages diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/history/ConversationHistory.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/history/ConversationHistory.java new file mode 100644 index 0000000000..8ece2d4eaa --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/history/ConversationHistory.java @@ -0,0 +1,26 @@ +package im.actor.core.modules.messaging.history; + +import im.actor.core.entity.Peer; +import im.actor.core.modules.ModuleContext; +import im.actor.runtime.actors.ActorInterface; +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.promise.Promise; + +import static im.actor.runtime.actors.ActorSystem.system; + +public class ConversationHistory extends ActorInterface { + + public ConversationHistory(Peer peer, ModuleContext context) { + setDest(system().actorOf("history/" + peer, () -> { + return new ConversationHistoryActor(peer, context); + })); + } + + public void loadMore() { + send(new ConversationHistoryActor.LoadMore()); + } + + public Promise reset() { + return ask(new ConversationHistoryActor.Reset()); + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/history/ConversationHistoryActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/history/ConversationHistoryActor.java index b76d4d000e..6ac9f1609d 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/history/ConversationHistoryActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/history/ConversationHistoryActor.java @@ -21,8 +21,11 @@ import im.actor.core.modules.api.ApiSupportConfiguration; import im.actor.core.modules.ModuleContext; import im.actor.core.modules.ModuleActor; +import im.actor.runtime.Log; +import im.actor.runtime.actors.ask.AskMessage; import im.actor.runtime.actors.messages.Void; import im.actor.runtime.function.Consumer; +import im.actor.runtime.promise.Promise; public class ConversationHistoryActor extends ModuleActor { @@ -39,7 +42,7 @@ public class ConversationHistoryActor extends ModuleActor { private long historyMaxDate; private boolean historyLoaded; - private boolean isLoading = false; + private boolean isFreezed = false; public ConversationHistoryActor(Peer peer, ModuleContext context) { super(context); @@ -60,22 +63,45 @@ public void preStart() { } private void onLoadMore() { - if (isLoading || historyLoaded) { + if (isFreezed || historyLoaded) { return; } - isLoading = true; + isFreezed = true; api(new RequestLoadHistory(buidOutPeer(peer), historyMaxDate, null, LIMIT, ApiSupportConfiguration.OPTIMIZATIONS)) .chain(r -> updates().applyRelatedData(r.getUsers(), r.getGroups())) .chain(r -> updates().loadRequiredPeers(r.getUserPeers(), r.getGroupPeers())) - .then(applyHistory(peer)) - .then(responseLoadHistory -> isLoading = false); + .flatMap(r -> { + Log.d("HistoryActor", "Apply " + historyMaxDate); + return applyHistory(peer, r.getHistory()); + }) + .map(r -> { + Log.d("HistoryActor", "Applied"); + isFreezed = false; + unstashAll(); + return null; + }); } - private Consumer applyHistory(final Peer peer) { - return responseLoadHistory -> applyHistory(peer, responseLoadHistory.getHistory()); + private Promise onReset() { + + Log.d("HistoryActor", "Reset"); + + historyMaxDate = Long.MAX_VALUE; + preferences().putLong(KEY_LOADED_DATE, Long.MAX_VALUE); + historyLoaded = false; + preferences().putBool(KEY_LOADED, false); + preferences().putBool(KEY_LOADED_INIT, false); + + isFreezed = true; + return context().getMessagesModule().getRouter().onChatReset(peer) + .then(r -> { + isFreezed = false; + unstashAll(); + onLoadMore(); + }); } - private void applyHistory(Peer peer, List history) { + private Promise applyHistory(Peer peer, List history) { ArrayList messages = new ArrayList<>(); long maxLoadedDate = Long.MAX_VALUE; @@ -106,9 +132,9 @@ private void applyHistory(Peer peer, List history) { // Sending updates to conversation actor final long finalMaxLoadedDate = maxLoadedDate; - context().getMessagesModule().getRouter() + return context().getMessagesModule().getRouter() .onChatHistoryLoaded(peer, messages, maxReceiveDate, maxReadDate, isEnded) - .then(r -> { + .map(r -> { // Saving Internal State if (isEnded) { historyLoaded = true; @@ -119,9 +145,25 @@ private void applyHistory(Peer peer, List history) { preferences().putLong(KEY_LOADED_DATE, finalMaxLoadedDate); preferences().putBool(KEY_LOADED, historyLoaded); preferences().putBool(KEY_LOADED_INIT, true); + return r; }); } + + @Override + public Promise onAsk(Object message) throws Exception { + if (message instanceof Reset) { + if (isFreezed) { + stash(); + return null; + } + onReset(); + return Promise.success(null); + } else { + return super.onAsk(message); + } + } + @Override public void onReceive(Object message) { if (message instanceof LoadMore) { @@ -134,4 +176,8 @@ public void onReceive(Object message) { public static class LoadMore { } + + public static class Reset implements AskMessage { + + } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/history/DialogsHistoryActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/history/DialogsHistoryActor.java index 1226b01337..d0abadd652 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/history/DialogsHistoryActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/history/DialogsHistoryActor.java @@ -44,6 +44,8 @@ public void preStart() { historyLoaded = preferences().getBool(KEY_LOADED, false); if (!preferences().getBool(KEY_LOADED_INIT, false)) { self().send(new LoadMore()); + } else { + context().getConductor().getConductor().onDialogsLoaded(); } } @@ -88,7 +90,6 @@ private void onLoadedMore(List rawDialogs) { } }); } else { - context().getAppStateModule().onDialogsLoaded(); markAsLoaded(); } } @@ -98,6 +99,7 @@ private void markAsLoaded() { historyLoaded = true; preferences().putBool(KEY_LOADED, true); preferences().putBool(KEY_LOADED_INIT, true); + context().getConductor().getConductor().onDialogsLoaded(); } private void markAsSliceLoaded(long date) { @@ -107,6 +109,7 @@ private void markAsSliceLoaded(long date) { preferences().putBool(KEY_LOADED, false); preferences().putBool(KEY_LOADED_INIT, true); preferences().putLong(KEY_LOADED_DATE, date); + context().getConductor().getConductor().onDialogsLoaded(); } // diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/RouterActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/RouterActor.java index f380f9aee9..b711a0dfe2 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/RouterActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/RouterActor.java @@ -11,6 +11,7 @@ import im.actor.core.api.rpc.RequestLoadGroupedDialogs; import im.actor.core.api.updates.UpdateChatClear; import im.actor.core.api.updates.UpdateChatDelete; +import im.actor.core.api.updates.UpdateChatDropCache; import im.actor.core.api.updates.UpdateChatGroupsChanged; import im.actor.core.api.updates.UpdateMessage; import im.actor.core.api.updates.UpdateMessageContentChanged; @@ -60,11 +61,13 @@ import im.actor.core.modules.messaging.router.entity.RouterOutgoingMessage; import im.actor.core.modules.messaging.router.entity.RouterOutgoingSent; import im.actor.core.modules.messaging.router.entity.RouterPeersChanged; +import im.actor.core.modules.messaging.router.entity.RouterResetChat; import im.actor.core.network.parser.Update; import im.actor.core.util.JavaUtil; import im.actor.core.viewmodel.DialogGroup; import im.actor.core.viewmodel.DialogSmall; import im.actor.core.viewmodel.generics.ArrayListDialogSmall; +import im.actor.runtime.Log; import im.actor.runtime.actors.messages.Void; import im.actor.runtime.promise.Promise; import im.actor.runtime.storage.KeyValueEngine; @@ -127,9 +130,11 @@ public void preStart() { showInvite = r.showInvite(); } onActiveDialogsChanged(r.getDialogs(), showArchived, showInvite); + context().getConductor().getConductor().onDialogsLoaded(); }); } else { notifyActiveDialogsVM(); + context().getConductor().getConductor().onDialogsLoaded(); } } @@ -210,6 +215,7 @@ private Promise onNewMessages(Peer peer, List messages) { ConversationState state = conversationStates.getValue(peer.getUnuqueId()); Message topMessage = null; int unreadCount = 0; + long maxInReadDate = 0; long maxInDate = 0; for (Message m : messages) { if (topMessage == null || topMessage.getSortDate() < m.getSortDate()) { @@ -218,6 +224,9 @@ private Promise onNewMessages(Peer peer, List messages) { if (m.getSenderId() != myUid()) { if (m.getSortDate() > state.getInReadDate()) { unreadCount++; + maxInReadDate = Math.max(maxInReadDate, m.getSortDate()); + } + if (m.getSortDate() > state.getInMaxMessageDate()) { maxInDate = Math.max(maxInDate, m.getSortDate()); } } @@ -242,24 +251,33 @@ private Promise onNewMessages(Peer peer, List messages) { if (unreadCount != 0) { if (isConversationVisible) { // Auto Reading message - if (maxInDate > 0) { - if (state.getInReadDate() < maxInDate) { - state = state.changeInReadDate(maxInDate); + boolean needUpdateState = false; + if (maxInReadDate > 0) { + if (state.getInReadDate() < maxInReadDate) { + state = state.changeInReadDate(maxInReadDate); } state = state.changeCounter(0); - if (state.getInMaxMessageDate() < maxInDate) { - state.changeInMaxDate(maxInDate); - } + context().getMessagesModule().getPlainReadActor() - .send(new CursorReaderActor.MarkRead(peer, maxInDate)); - context().getNotificationsModule().onOwnRead(peer, maxInDate); + .send(new CursorReaderActor.MarkRead(peer, maxInReadDate)); + context().getNotificationsModule().onOwnRead(peer, maxInReadDate); isRead = true; + needUpdateState = true; + } + + if (state.getInMaxMessageDate() < maxInDate) { + state.changeInMaxDate(maxInDate); + needUpdateState = true; + } + + if (needUpdateState) { conversationStates.addOrUpdateItem(state); } + } else { // Updating counter state = state.changeCounter(state.getUnreadCount() + unreadCount); - if (maxInDate > state.getInMaxMessageDate()) { + if (state.getInMaxMessageDate() < maxInDate) { state = state .changeInMaxDate(maxInDate); } @@ -273,9 +291,9 @@ private Promise onNewMessages(Peer peer, List messages) { // // Marking As Received // - if (maxInDate > 0 && !isRead) { + if (maxInReadDate > 0 && !isRead) { context().getMessagesModule().getPlainReceiverActor() - .send(new CursorReceiverActor.MarkReceived(peer, maxInDate)); + .send(new CursorReceiverActor.MarkReceived(peer, maxInReadDate)); } @@ -401,6 +419,8 @@ private Promise onDialogHistoryLoaded(List dialogs) { private Promise onChatHistoryLoaded(Peer peer, List messages, Long maxReadDate, Long maxReceiveDate, boolean isEnded) { + Log.d(TAG, "History Loaded"); + long maxMessageDate = 0; // Processing all new messages @@ -497,7 +517,19 @@ private Promise onMessageDeleted(Peer peer, List rids) { Message head = conversation(peer).getHeadValue(); - return getDialogsRouter().onMessageDeleted(peer, head.getMessageState() == MessageState.PENDING ? null : head); + if (head != null) { + ConversationState state = conversationStates.getValue(peer.getUnuqueId()); + state = state + .changeInReadDate(head.getSortDate()) + .changeOutSendDate(head.getSortDate()); + conversationStates.addOrUpdateItem(state); + + if (head.getMessageState() == MessageState.PENDING) { + head = null; + } + } + + return getDialogsRouter().onMessageDeleted(peer, head); } private Promise onChatClear(Peer peer) { @@ -515,6 +547,25 @@ private Promise onChatClear(Peer peer) { return getDialogsRouter().onChatClear(peer); } + private Promise onChatDropCache(Peer peer) { + return context().getMessagesModule().getHistoryActor(peer).reset(); + } + + private Promise onChatReset(Peer peer) { + + Log.d(TAG, "onChatReset"); + + conversation(peer).clear(); + + ConversationState state = conversationStates.getValue(peer.getUnuqueId()); + state = state.changeIsLoaded(false); + conversationStates.addOrUpdateItem(state); + + updateChatState(peer); + + return Promise.success(null); + } + private Promise onChatDelete(Peer peer) { conversation(peer).clear(); @@ -527,7 +578,7 @@ private Promise onChatDelete(Peer peer) { updateChatState(peer); - return getDialogsRouter().onChatDelete(peer); + return getDialogsRouter().onChatDelete(peer).chain(aVoid -> onChatDropCache(peer)); } @@ -693,8 +744,9 @@ private void updateChatState(Peer peer) { ConversationState state = conversationStates.getValue(peer.getUnuqueId()); if (state.isEmpty() != isEmpty) { state = state.changeIsEmpty(isEmpty); - conversationStates.addOrUpdateItem(state); } + + conversationStates.addOrUpdateItem(state); } // @@ -751,7 +803,7 @@ private void notifyActiveDialogsVM() { groups.add(new DialogGroup(i.getTitle(), i.getKey(), dialogSmalls)); } context().getMessagesModule().getDialogGroupsVM().getGroupsValueModel().change(groups); - context().getAppStateModule().getGlobalStateVM().onGlobalCounterChanged(counter); + context().getConductor().getGlobalStateVM().onGlobalCounterChanged(counter); } public boolean isValidPeer(Peer peer) { @@ -795,7 +847,7 @@ public Promise onUpdate(Update update) { context().getMessagesModule() .getSendMessageActor() .send(new SenderActor.MessageSent(peer, messageSent.getRid())); - onOutgoingSent( + return onOutgoingSent( peer, messageSent.getRid(), messageSent.getDate()); @@ -805,7 +857,7 @@ public Promise onUpdate(Update update) { UpdateMessageRead read = (UpdateMessageRead) update; Peer peer = convert(read.getPeer()); if (isValidPeer(peer)) { - onMessageRead(peer, read.getStartDate()); + return onMessageRead(peer, read.getStartDate()); } return Promise.success(null); } else if (update instanceof UpdateMessageReadByMe) { @@ -816,28 +868,35 @@ public Promise onUpdate(Update update) { if (readByMe.getUnreadCounter() != null) { counter = readByMe.getUnreadCounter(); } - onMessageReadByMe(peer, readByMe.getStartDate(), counter); + return onMessageReadByMe(peer, readByMe.getStartDate(), counter); } return Promise.success(null); } else if (update instanceof UpdateMessageReceived) { UpdateMessageReceived received = (UpdateMessageReceived) update; Peer peer = convert(received.getPeer()); if (isValidPeer(peer)) { - onMessageReceived(peer, received.getStartDate()); + return onMessageReceived(peer, received.getStartDate()); } return Promise.success(null); } else if (update instanceof UpdateChatDelete) { UpdateChatDelete delete = (UpdateChatDelete) update; Peer peer = convert(delete.getPeer()); if (isValidPeer(peer)) { - onChatDelete(peer); + return onChatDelete(peer); } return Promise.success(null); } else if (update instanceof UpdateChatClear) { UpdateChatClear clear = (UpdateChatClear) update; Peer peer = convert(clear.getPeer()); if (isValidPeer(peer)) { - onChatClear(peer); + return onChatClear(peer); + } + return Promise.success(null); + } else if (update instanceof UpdateChatDropCache) { + UpdateChatDropCache dropCache = (UpdateChatDropCache) update; + Peer peer = convert(dropCache.getPeer()); + if (isValidPeer(peer)) { + return onChatDropCache(peer); } return Promise.success(null); } else if (update instanceof UpdateChatGroupsChanged) { @@ -848,7 +907,7 @@ public Promise onUpdate(Update update) { UpdateMessageDelete delete = (UpdateMessageDelete) update; Peer peer = convert(delete.getPeer()); if (isValidPeer(peer)) { - onMessageDeleted(peer, delete.getRids()); + return onMessageDeleted(peer, delete.getRids()); } return Promise.success(null); } else if (update instanceof UpdateMessageContentChanged) { @@ -856,7 +915,7 @@ public Promise onUpdate(Update update) { Peer peer = convert(contentChanged.getPeer()); if (isValidPeer(peer)) { AbsContent content = AbsContent.fromMessage(contentChanged.getMessage()); - onContentUpdate(peer, contentChanged.getRid(), content); + return onContentUpdate(peer, contentChanged.getRid(), content); } return Promise.success(null); } else if (update instanceof UpdateReactionsUpdate) { @@ -867,7 +926,7 @@ public Promise onUpdate(Update update) { for (ApiMessageReaction r : reactionsUpdate.getReactions()) { reactions.add(new Reaction(r.getCode(), r.getUsers())); } - onReactionsUpdate(peer, reactionsUpdate.getRid(), reactions); + return onReactionsUpdate(peer, reactionsUpdate.getRid(), reactions); } return Promise.success(null); } @@ -954,6 +1013,9 @@ public Promise onAsk(Object message) throws Exception { return onMessageDeleted( routerDeletedMessages.getPeer(), routerDeletedMessages.getRids()); + } else if (message instanceof RouterResetChat) { + RouterResetChat resetChat = (RouterResetChat) message; + return onChatReset(resetChat.getPeer()); } else { return super.onAsk(message); } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/RouterInt.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/RouterInt.java index df8370adc0..1bc228aba6 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/RouterInt.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/RouterInt.java @@ -28,6 +28,7 @@ import im.actor.core.modules.messaging.router.entity.RouterOutgoingError; import im.actor.core.modules.messaging.router.entity.RouterOutgoingMessage; import im.actor.core.modules.messaging.router.entity.RouterPeersChanged; +import im.actor.core.modules.messaging.router.entity.RouterResetChat; import im.actor.core.network.parser.Update; import im.actor.runtime.actors.ActorInterface; import im.actor.runtime.actors.messages.Void; @@ -150,6 +151,10 @@ public Promise onPeersChanged(List users, List groups) { return ask(new RouterPeersChanged(users, groups)); } + // Resetting + public Promise onChatReset(Peer peer) { + return ask(new RouterResetChat(peer)); + } @Override public void onBusEvent(Event event) { diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterResetChat.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterResetChat.java new file mode 100644 index 0000000000..cc00cec447 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/messaging/router/entity/RouterResetChat.java @@ -0,0 +1,18 @@ +package im.actor.core.modules.messaging.router.entity; + +import im.actor.core.entity.Peer; +import im.actor.runtime.actors.ask.AskMessage; +import im.actor.runtime.actors.messages.Void; + +public class RouterResetChat implements AskMessage { + + private Peer peer; + + public RouterResetChat(Peer peer) { + this.peer = peer; + } + + public Peer getPeer() { + return peer; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/misc/AppStateModule.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/misc/AppStateModule.java deleted file mode 100644 index eba6428c2e..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/misc/AppStateModule.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2015 Actor LLC. - */ - -package im.actor.core.modules.misc; - -import im.actor.core.api.ApiAppCounters; -import im.actor.core.modules.AbsModule; -import im.actor.core.modules.ModuleContext; -import im.actor.core.viewmodel.AppStateVM; -import im.actor.core.viewmodel.GlobalStateVM; -import im.actor.runtime.actors.ActorCreator; -import im.actor.runtime.actors.ActorRef; - -import static im.actor.runtime.actors.ActorSystem.system; - -public class AppStateModule extends AbsModule { - - private AppStateVM appStateVM; - private GlobalStateVM globalStateVM; - private ActorRef listStatesActor; - - public AppStateModule(ModuleContext context) { - super(context); - - globalStateVM = new GlobalStateVM(context); - } - - public void run() { - this.appStateVM = new AppStateVM(context()); - listStatesActor = system().actorOf("actor/app/state", () -> new ListsStatesActor(context())); - } - - public void onDialogsUpdate(boolean isEmpty) { - listStatesActor.send(new ListsStatesActor.OnDialogsChanged(isEmpty)); - } - - public void onContactsUpdate(boolean isEmpty) { - listStatesActor.send(new ListsStatesActor.OnContactsChanged(isEmpty)); - } - - public void onBookImported() { - listStatesActor.send(new ListsStatesActor.OnBookImported()); - } - - public void onContactsLoaded() { - listStatesActor.send(new ListsStatesActor.OnContactsLoaded()); - } - - public void onDialogsLoaded() { - listStatesActor.send(new ListsStatesActor.OnDialogsLoaded()); - } - - public AppStateVM getAppStateVM() { - return appStateVM; - } - - public GlobalStateVM getGlobalStateVM() { - return globalStateVM; - } - - public void resetModule() { - // TODO: Implement - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/misc/DeviceInfoActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/misc/DeviceInfoActor.java deleted file mode 100644 index c40aa8076b..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/misc/DeviceInfoActor.java +++ /dev/null @@ -1,68 +0,0 @@ -package im.actor.core.modules.misc; - -import java.util.ArrayList; - -import im.actor.core.api.rpc.RequestNotifyAboutDeviceInfo; -import im.actor.core.api.rpc.ResponseVoid; -import im.actor.core.modules.ModuleContext; -import im.actor.core.modules.ModuleActor; -import im.actor.core.network.RpcCallback; -import im.actor.core.network.RpcException; -import im.actor.core.util.JavaUtil; - -public class DeviceInfoActor extends ModuleActor { - - public DeviceInfoActor(ModuleContext context) { - super(context); - } - - @Override - public void preStart() { - super.preStart(); - - // - // Loading Information - // - ArrayList langs = new ArrayList<>(); - for (String s : context().getConfiguration().getPreferredLanguages()) { - langs.add(s); - } - final String timeZone = context().getConfiguration().getTimeZone(); - - // - // Checking if information changed - // - String expectedLangs = ""; - for (String s : langs) { - if (!"".equals(expectedLangs)) { - expectedLangs += ","; - } - expectedLangs += s.toLowerCase(); - } - - if (expectedLangs.equals(preferences().getString("device_info_langs")) && - JavaUtil.equalsE(timeZone, preferences().getString("device_info_timezone"))) { - // Already sent - return; - } - - // - // Performing Notification - // - final String finalExpectedLangs = expectedLangs; - request(new RequestNotifyAboutDeviceInfo(langs, timeZone), new RpcCallback() { - @Override - public void onResult(ResponseVoid response) { - - // Mark as sent - preferences().putString("device_info_langs", finalExpectedLangs); - preferences().putString("device_info_timezone", timeZone); - } - - @Override - public void onError(RpcException e) { - // Ignoring error - } - }); - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/misc/DeviceInfoModule.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/misc/DeviceInfoModule.java deleted file mode 100644 index 25ff9e6f9c..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/misc/DeviceInfoModule.java +++ /dev/null @@ -1,23 +0,0 @@ -package im.actor.core.modules.misc; - -import im.actor.core.modules.AbsModule; -import im.actor.core.modules.ModuleContext; -import im.actor.core.modules.misc.DeviceInfoActor; -import im.actor.runtime.actors.ActorCreator; -import im.actor.runtime.actors.ActorRef; -import im.actor.runtime.actors.Props; - -import static im.actor.runtime.actors.ActorSystem.system; - -public class DeviceInfoModule extends AbsModule { - - private ActorRef actorRef; - - public DeviceInfoModule(ModuleContext context) { - super(context); - } - - public void run() { - actorRef = system().actorOf("device_info/notifier", () -> new DeviceInfoActor(context())); - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/misc/ListsStatesActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/misc/ListsStatesActor.java deleted file mode 100644 index 79926383fd..0000000000 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/misc/ListsStatesActor.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2015 Actor LLC. - */ - -package im.actor.core.modules.misc; - -import im.actor.core.modules.ModuleContext; -import im.actor.core.modules.ModuleActor; - -public class ListsStatesActor extends ModuleActor { - - public ListsStatesActor(ModuleContext context) { - super(context); - } - - public void onDialogsChanged(boolean isEmpty) { - context().getAppStateModule().getAppStateVM().onDialogsChanged(isEmpty); - } - - public void onContactsChanged(boolean isEmpty) { - context().getAppStateModule().getAppStateVM().onContactsChanged(isEmpty); - } - - public void onBookImported() { - context().getAppStateModule().getAppStateVM().onPhoneImported(); - } - - public void onContactsLoaded() { - context().getAppStateModule().getAppStateVM().onContactsLoaded(); - } - - public void onDialogsLoaded() { - context().getAppStateModule().getAppStateVM().onDialogsLoaded(); - } - - @Override - public void onReceive(Object message) { - if (message instanceof OnContactsChanged) { - onContactsChanged(((OnContactsChanged) message).isEmpty()); - } else if (message instanceof OnDialogsChanged) { - onDialogsChanged(((OnDialogsChanged) message).isEmpty()); - } else if (message instanceof OnBookImported) { - onBookImported(); - } else if (message instanceof OnContactsLoaded) { - onContactsLoaded(); - } else if (message instanceof OnDialogsLoaded) { - onDialogsLoaded(); - } else { - super.onReceive(message); - } - } - - public static class OnBookImported { - - } - - public static class OnContactsLoaded { - - } - - public static class OnDialogsLoaded { - - } - - public static class OnContactsChanged { - private boolean isEmpty; - - public OnContactsChanged(boolean isEmpty) { - this.isEmpty = isEmpty; - } - - public boolean isEmpty() { - return isEmpty; - } - } - - public static class OnDialogsChanged { - private boolean isEmpty; - - public OnDialogsChanged(boolean isEmpty) { - this.isEmpty = isEmpty; - } - - public boolean isEmpty() { - return isEmpty; - } - } -} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/notifications/NotificationsActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/notifications/NotificationsActor.java index 20add7552b..95af715229 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/notifications/NotificationsActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/notifications/NotificationsActor.java @@ -12,6 +12,7 @@ import im.actor.core.PlatformType; import im.actor.core.entity.ContentDescription; +import im.actor.core.entity.GroupType; import im.actor.core.entity.Notification; import im.actor.core.entity.Peer; import im.actor.core.entity.PeerType; @@ -31,7 +32,7 @@ /** * Actor that controls all notifications in application - *

+ *

* NotificationsActor keeps all unread messages for showing last unread messages in notifications * Actor also control sound effects playing logic */ @@ -407,7 +408,11 @@ private void performNotificationImp(boolean performUpdate) { // Converting to PendingNotifications List res = new ArrayList<>(); for (PendingNotification p : destNotifications) { - res.add(new Notification(p.getPeer(), p.getSender(), p.getContent())); + boolean isChannel = false; + if (p.getPeer().getPeerType() == PeerType.GROUP) { + isChannel = groups().getValue(p.getPeer().getPeerId()).getGroupType() == GroupType.CHANNEL; + } + res.add(new Notification(p.getPeer(), isChannel, p.getSender(), p.getContent())); } // Performing notifications diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/presence/PresenceActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/presence/PresenceActor.java index af2ed4317e..e2998d9ebf 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/presence/PresenceActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/presence/PresenceActor.java @@ -15,6 +15,7 @@ import im.actor.core.api.rpc.RequestSubscribeToOnline; import im.actor.core.api.rpc.ResponseVoid; import im.actor.core.entity.Group; +import im.actor.core.entity.GroupType; import im.actor.core.entity.Peer; import im.actor.core.entity.PeerType; import im.actor.core.entity.User; @@ -179,6 +180,7 @@ private void subscribe(Peer peer) { // Log.d(TAG, "subscribe:" + peer); if (peer.getPeerType() == PeerType.PRIVATE) { + // Already subscribed if (uids.contains(peer.getPeerId())) { return; @@ -193,6 +195,7 @@ private void subscribe(Peer peer) { uids.add(user.getUid()); } else if (peer.getPeerType() == PeerType.GROUP) { + // Already subscribed if (gids.contains(peer.getPeerId())) { return; @@ -203,6 +206,11 @@ private void subscribe(Peer peer) { return; } + // Ignore subscription to channels + if (group.getGroupType() == GroupType.CHANNEL) { + return; + } + // Subscribing to group online sates gids.add(peer.getPeerId()); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/profile/avatar/GroupAvatarChangeActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/profile/avatar/GroupAvatarChangeActor.java index c4243f9a2f..df6fbbc424 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/profile/avatar/GroupAvatarChangeActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/profile/avatar/GroupAvatarChangeActor.java @@ -72,11 +72,8 @@ public void uploadCompleted(final long rid, FileReference fileReference) { updates().applyUpdate( responseEditGroupAvatar.getSeq(), responseEditGroupAvatar.getState(), - new UpdateGroupAvatarChangedObsolete( - gid, rid, myUid(), - responseEditGroupAvatar.getAvatar(), - responseEditGroupAvatar.getDate()) - )) + new UpdateGroupAvatarChanged( + gid, responseEditGroupAvatar.getAvatar()))) .then(v -> avatarChanged(gid, rid)) .failure(e -> { if (!tasksMap.containsKey(rid)) { diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/raw/RawProcessor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/raw/RawProcessor.java new file mode 100644 index 0000000000..5d9e95a322 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/raw/RawProcessor.java @@ -0,0 +1,45 @@ +package im.actor.core.modules.raw; + + +import im.actor.core.RawUpdatesHandler; +import im.actor.core.api.updates.UpdateRawUpdate; +import im.actor.core.modules.AbsModule; +import im.actor.core.modules.ModuleContext; +import im.actor.core.modules.sequence.processor.SequenceProcessor; +import im.actor.core.network.parser.Update; +import im.actor.runtime.Log; +import im.actor.runtime.actors.messages.Void; +import im.actor.runtime.promise.Promise; +import im.actor.runtime.Runtime; + +public class RawProcessor extends AbsModule implements SequenceProcessor { + + private final RawUpdatesHandler rawUpdatesHandler; + + public RawProcessor(ModuleContext context) { + super(context); + rawUpdatesHandler = context().getConfiguration().getRawUpdatesHandler(); + + } + + @Override + public Promise process(Update update) { + if (update instanceof UpdateRawUpdate && rawUpdatesHandler != null) { + return new Promise<>(resolver -> { + Runtime.dispatch(() -> { + try { + Promise promise = rawUpdatesHandler.onRawUpdate((UpdateRawUpdate) update); + if (promise != null) { + promise.pipeTo(resolver); + } + } catch (Exception e) { + Log.e("RawUpdateHandler", e); + } finally { + resolver.result(null); + } + }); + }); + } + return null; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/search/SearchModule.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/search/SearchModule.java index face3199c0..99658ab19f 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/search/SearchModule.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/search/SearchModule.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.List; +import im.actor.core.api.ApiGroupOutPeer; import im.actor.core.api.ApiSearchAndCondition; import im.actor.core.api.ApiSearchCondition; import im.actor.core.api.ApiSearchContentType; @@ -23,13 +24,17 @@ import im.actor.core.entity.PeerSearchEntity; import im.actor.core.entity.PeerSearchType; import im.actor.core.entity.SearchEntity; +import im.actor.core.entity.SearchResult; import im.actor.core.entity.content.AbsContent; import im.actor.core.modules.AbsModule; import im.actor.core.modules.api.ApiSupportConfiguration; import im.actor.core.modules.Modules; +import im.actor.core.modules.search.sources.GlobalSearchSource; import im.actor.runtime.Storage; import im.actor.runtime.actors.ActorRef; +import im.actor.runtime.actors.messages.Void; import im.actor.runtime.collections.ManagedList; +import im.actor.runtime.mvvm.SearchValueModel; import im.actor.runtime.promise.Promise; import im.actor.runtime.storage.ListEngine; @@ -38,6 +43,9 @@ public class SearchModule extends AbsModule { + // j2objc workaround + private static final Void DUMB = null; + private ListEngine searchList; private ActorRef actorRef; @@ -100,7 +108,7 @@ private Promise> findMessages(final ApiSearchCondition AbsContent.fromMessage(itm.getResult().getContent())))); } - public Promise> findPeers(final PeerSearchType type) { + public Promise> findPeers(PeerSearchType type) { final ApiSearchPeerType apiType; if (type == PeerSearchType.GROUPS) { apiType = ApiSearchPeerType.GROUPS; @@ -111,19 +119,38 @@ public Promise> findPeers(final PeerSearchType type) { } ArrayList conditions = new ArrayList<>(); conditions.add(new ApiSearchPeerTypeCondition(apiType)); + return findPeers(conditions); + } + + public Promise> findPeers(String query) { + ArrayList conditions = new ArrayList<>(); + conditions.add(new ApiSearchPieceText(query)); + return findPeers(conditions); + } + + public Promise> findPeers(ArrayList conditions) { return api(new RequestPeerSearch(conditions, ApiSupportConfiguration.OPTIMIZATIONS)) .chain(responsePeerSearch -> updates().applyRelatedData( responsePeerSearch.getUsers(), responsePeerSearch.getGroups())) + .chain(responsePeerSearch2 -> + updates().loadRequiredPeers(responsePeerSearch2.getUserPeers(), responsePeerSearch2.getGroupPeers())) .map(responsePeerSearch1 -> ManagedList.of(responsePeerSearch1.getSearchResults()) - .map(r -> new PeerSearchEntity(convert(r.getPeer()), r.getTitle(), - r.getDescription(), r.getMembersCount(), r.getDateCreated(), - r.getCreator(), r.isPublic(), r.isJoined()))); + .map(r -> new PeerSearchEntity(convert(r.getPeer()), r.getOptMatchString()))); } + public Promise findPublicGroupById(int gid) { + ArrayList groups = new ArrayList<>(); + groups.add(new ApiGroupOutPeer(gid, 0)); + return updates().loadRequiredPeers(new ArrayList<>(), groups).map(aVoid -> Peer.group(gid)); + } + + public SearchValueModel buildSearchModel() { + return new SearchValueModel<>(new GlobalSearchSource(context())); + } // // Local Search diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/search/sources/GlobalSearchSource.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/search/sources/GlobalSearchSource.java new file mode 100644 index 0000000000..888893ed90 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/search/sources/GlobalSearchSource.java @@ -0,0 +1,78 @@ +package im.actor.core.modules.search.sources; + +import java.util.ArrayList; +import java.util.List; + +import im.actor.core.entity.Group; +import im.actor.core.entity.PeerSearchEntity; +import im.actor.core.entity.PeerType; +import im.actor.core.entity.SearchEntity; +import im.actor.core.entity.SearchResult; +import im.actor.core.modules.AbsModule; +import im.actor.core.modules.ModuleContext; +import im.actor.core.viewmodel.UserVM; +import im.actor.runtime.function.Consumer; +import im.actor.runtime.mvvm.SearchValueSource; +import im.actor.runtime.storage.ListEngine; +import im.actor.runtime.storage.ListEngineDisplayExt; + +public class GlobalSearchSource extends AbsModule implements SearchValueSource { + + public GlobalSearchSource(ModuleContext context) { + super(context); + } + + @Override + public void loadResults(String query, Consumer> callback) { + ListEngine searchList = context().getSearchModule().getSearchList(); + if (searchList instanceof ListEngineDisplayExt) { + ((ListEngineDisplayExt) searchList).loadBackward(query, 20, (items, topSortKey, bottomSortKey) -> { + ArrayList localResults = new ArrayList<>(); + for (SearchEntity e : items) { + localResults.add(new SearchResult(e.getPeer(), e.getAvatar(), e.getTitle(), + null)); + } + callback.apply(new ArrayList<>(localResults)); + if (query.length() > 3) { + loadGlobalResults(query, localResults, callback); + } + }); + } else { + if (query.length() > 3) { + loadGlobalResults(query, new ArrayList<>(), callback); + } else { + callback.apply(new ArrayList<>()); + } + } + } + + + private void loadGlobalResults(String query, ArrayList localResults, Consumer> callback) { + context().getSearchModule().findPeers(query).then(r -> { + ArrayList results = new ArrayList<>(); + outer: + for (PeerSearchEntity peerSearch : r) { + for (SearchResult l : localResults) { + if (peerSearch.getPeer().equals(l.getPeer())) { + continue outer; + } + } + if (peerSearch.getPeer().getPeerType() == PeerType.GROUP) { + Group group = context().getGroupsModule().getGroups().getValue(peerSearch.getPeer().getPeerId()); + results.add(new SearchResult(peerSearch.getPeer(), group.getAvatar(), group.getTitle(), + peerSearch.getOptMatchString())); + } else if (peerSearch.getPeer().getPeerType() == PeerType.PRIVATE) { + UserVM user = context().getUsersModule().getUsers().get(peerSearch.getPeer().getPeerId()); + results.add(new SearchResult(peerSearch.getPeer(), user.getAvatar().get(), user.getName().get(), + peerSearch.getOptMatchString())); + } + } + if (results.size() > 0) { + ArrayList combined = new ArrayList<>(); + combined.addAll(localResults); + combined.addAll(results); + callback.apply(combined); + } + }); + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/SequenceActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/SequenceActor.java index 7d76f08571..6e68474160 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/SequenceActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/SequenceActor.java @@ -43,6 +43,7 @@ public static ActorCreator CONSTRUCTOR(final ModuleContext context) { private static final String TAG = "Updates"; private static final int INVALIDATE_GAP = 2000;// 2 Secs private static final int INVALIDATE_MAX_SEC_HOLE = 10; + private static final boolean PROCESS_EXTERNAL_PUSH_SEQ = false; private static final String KEY_SEQ = "updates_seq"; private static final String KEY_STATE = "updates_state"; @@ -100,13 +101,21 @@ private void onWeakUpdateReceived(int type, byte[] body, long date) { handler.onWeakUpdate(update, date); } - private void onPushSeqReceived(int seq) { - if (seq <= this.seq) { - Log.d(TAG, "Ignored PushSeq {seq:" + seq + "}"); - } else { - Log.w(TAG, "External Out of sequence: starting timer for invalidation"); - startInvalidationTimer(); + private void onPushSeqReceived(int seq, long authId) { + if (context().getApiModule() == null) { + return; } + context().getApiModule().checkIsCurrentAuthId(authId).then(same -> { + if (same) { + if (seq <= this.seq) { + Log.d(TAG, "Ignored PushSeq {seq:" + seq + "}"); + } else { + Log.w(TAG, "External Out of sequence: starting timer for invalidation"); + startInvalidationTimer(); + } + } + }); + } @Deprecated @@ -302,11 +311,11 @@ private void checkRunnables() { // private void onUpdateStarted() { - context().getAppStateModule().getGlobalStateVM().getIsSyncing().change(true); + context().getConductor().getGlobalStateVM().getIsSyncing().change(true); } private void onUpdateEnded() { - context().getAppStateModule().getGlobalStateVM().getIsSyncing().change(false); + context().getConductor().getGlobalStateVM().getIsSyncing().change(false); } // @@ -413,7 +422,7 @@ public void onReceive(Object message) { stash(); return; } - onPushSeqReceived(((PushSeq) message).seq); + onPushSeqReceived(((PushSeq) message).seq, ((PushSeq) message).authId); } else if (message instanceof WeakUpdate) { WeakUpdate weakUpdate = (WeakUpdate) message; onWeakUpdateReceived(weakUpdate.getUpdateHeader(), weakUpdate.getUpdate(), @@ -432,9 +441,11 @@ public static class Invalidate { } public static class PushSeq { + private long authId; private int seq; - public PushSeq(int seq) { + public PushSeq(int seq, long authId) { + this.authId = authId; this.seq = seq; } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/SequenceHandlerActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/SequenceHandlerActor.java index 21da5d001c..654a7b1c87 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/SequenceHandlerActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/SequenceHandlerActor.java @@ -78,7 +78,10 @@ private Promise onSeqUpdate(final Update update, // Update Application currentPromise = currentPromise - .chain(v -> processor.processUpdate(update)); + .chain(v -> processor.processUpdate(update)) + .then(v -> { + Log.d(TAG, "Ended processing update: " + update); + }); // Handling update end currentPromise.then(v -> endUpdates()); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/Updates.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/Updates.java index 9f0a465fca..e2157d8ee2 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/Updates.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/Updates.java @@ -4,6 +4,8 @@ package im.actor.core.modules.sequence; +import org.jetbrains.annotations.NotNull; + import java.util.ArrayList; import java.util.List; @@ -25,6 +27,7 @@ import im.actor.runtime.eventbus.Event; import im.actor.runtime.promise.Promise; import im.actor.runtime.promise.PromiseFunc; +import im.actor.runtime.promise.PromiseResolver; import im.actor.runtime.promise.Promises; import im.actor.runtime.promise.PromisesArray; @@ -56,13 +59,12 @@ public SequenceHandlerInt getUpdateHandler() { return updateHandlerInt; } - public void onPushReceived(int seq) { + public void onPushReceived(int seq, long authId) { if (updateActor != null) { - updateActor.send(new SequenceActor.PushSeq(seq)); + updateActor.send(new SequenceActor.PushSeq(seq, authId)); } } - public Promise applyUpdate(int seq, byte[] state, Update update) { return new Promise<>((PromiseFunc) resolver -> { updateActor.send(new SeqUpdate(seq, state, update.getHeaderKey(), update.toByteArray())); @@ -75,7 +77,6 @@ public Promise applyUpdate(int seq, byte[] state, Update update, ArrayList groups = new ArrayList<>(); groups.add(group); return applyUpdate(seq, state, update, users, groups); - } public Promise applyUpdate(int seq, byte[] state, Update update, @@ -87,6 +88,11 @@ public Promise applyUpdate(int seq, byte[] state, Update update, }); } + public Promise waitForUpdate(int seq) { + return new Promise<>((PromiseFunc) resolver -> { + executeAfter(seq, () -> resolver.result(null)); + }); + } public Promise applyRelatedData(final List users) { return applyRelatedData(users, new ArrayList<>()); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/internal/GetDiffCombiner.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/internal/GetDiffCombiner.java index d1330a635f..c132ada335 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/internal/GetDiffCombiner.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/internal/GetDiffCombiner.java @@ -1,7 +1,11 @@ package im.actor.core.modules.sequence.internal; +import java.util.ArrayList; import java.util.List; +import im.actor.core.api.ApiDialogGroup; +import im.actor.core.api.ApiDialogShort; +import im.actor.core.api.updates.UpdateChatGroupsChanged; import im.actor.core.api.updates.UpdateCountersChanged; import im.actor.core.api.updates.UpdateMessage; import im.actor.core.api.updates.UpdateMessageRead; @@ -19,6 +23,7 @@ public class GetDiffCombiner { public static CombinedDifference buildDiff(List updates) { CombinedDifference res = new CombinedDifference(); + UpdateChatGroupsChanged chatGroupsChanged = null; for (Update u : updates) { if (u instanceof UpdateMessage) { res.putMessage((UpdateMessage) u); @@ -37,10 +42,32 @@ public static CombinedDifference buildDiff(List updates) { res.putReadByMe(convert(readByMe.getPeer()), readByMe.getStartDate(), counter); } else if (u instanceof UpdateCountersChanged) { // Ignore + } else if (u instanceof UpdateChatGroupsChanged) { + chatGroupsChanged = (UpdateChatGroupsChanged) u; } else { res.getOtherUpdates().add(u); } } + + // Handling reordering of updates + if (chatGroupsChanged != null) { + ArrayList dialogs = new ArrayList<>(); + for (ApiDialogGroup dg : chatGroupsChanged.getDialogs()) { + ArrayList dialogShorts = new ArrayList<>(); + for (ApiDialogShort ds : dg.getDialogs()) { + CombinedDifference.ReadByMeValue val = res.getReadByMe().get(convert(ds.getPeer())); + if (val != null) { + dialogShorts.add(new ApiDialogShort(ds.getPeer(), + val.getCounter(), ds.getDate())); + } else { + dialogShorts.add(ds); + } + } + dialogs.add(new ApiDialogGroup(dg.getTitle(), dg.getKey(), dialogShorts)); + } + res.getOtherUpdates().add(new UpdateChatGroupsChanged(dialogs)); + } + return res; } } \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/processor/UpdateProcessor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/processor/UpdateProcessor.java index 90a9637000..3c95412bc3 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/processor/UpdateProcessor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/processor/UpdateProcessor.java @@ -23,6 +23,7 @@ import im.actor.core.modules.eventbus.EventBusProcessor; import im.actor.core.modules.groups.GroupsProcessor; import im.actor.core.modules.presence.PresenceProcessor; +import im.actor.core.modules.raw.RawProcessor; import im.actor.core.modules.settings.SettingsProcessor; import im.actor.core.modules.stickers.StickersProcessor; import im.actor.core.modules.typing.TypingProcessor; @@ -65,7 +66,8 @@ public UpdateProcessor(ModuleContext context) { new ContactsProcessor(context), new EncryptedProcessor(context), new StickersProcessor(context), - new SettingsProcessor(context) + new SettingsProcessor(context), + new RawProcessor(context) }; this.typingProcessor = new TypingProcessor(context); diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/processor/UpdateValidator.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/processor/UpdateValidator.java index 541e07e713..21f55884fd 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/processor/UpdateValidator.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/sequence/processor/UpdateValidator.java @@ -5,15 +5,20 @@ import im.actor.core.api.ApiDialogGroup; import im.actor.core.api.ApiDialogShort; +import im.actor.core.api.ApiMember; import im.actor.core.api.ApiPeerType; import im.actor.core.api.updates.UpdateChatGroupsChanged; import im.actor.core.api.updates.UpdateContactRegistered; import im.actor.core.api.updates.UpdateContactsAdded; import im.actor.core.api.updates.UpdateContactsRemoved; -import im.actor.core.api.updates.UpdateGroupInvite; -import im.actor.core.api.updates.UpdateGroupUserInvited; -import im.actor.core.api.updates.UpdateGroupUserKick; -import im.actor.core.api.updates.UpdateGroupUserLeave; +import im.actor.core.api.updates.UpdateGroupExtChanged; +import im.actor.core.api.updates.UpdateGroupFullExtChanged; +import im.actor.core.api.updates.UpdateGroupMemberAdminChanged; +import im.actor.core.api.updates.UpdateGroupMemberChanged; +import im.actor.core.api.updates.UpdateGroupMemberDiff; +import im.actor.core.api.updates.UpdateGroupMembersCountChanged; +import im.actor.core.api.updates.UpdateGroupMembersUpdated; +import im.actor.core.api.updates.UpdateGroupOwnerChanged; import im.actor.core.api.updates.UpdateMessage; import im.actor.core.api.updates.UpdateUserLocalNameChanged; import im.actor.core.modules.AbsModule; @@ -44,24 +49,6 @@ public boolean isCausesInvalidation(Update update) { } else if (update instanceof UpdateContactRegistered) { UpdateContactRegistered contactRegistered = (UpdateContactRegistered) update; users.add(contactRegistered.getUid()); - } else if (update instanceof UpdateGroupInvite) { - UpdateGroupInvite groupInvite = (UpdateGroupInvite) update; - users.add(groupInvite.getInviteUid()); - groups.add(groupInvite.getGroupId()); - } else if (update instanceof UpdateGroupUserInvited) { - UpdateGroupUserInvited invited = (UpdateGroupUserInvited) update; - users.add(invited.getInviterUid()); - users.add(invited.getUid()); - groups.add(invited.getGroupId()); - } else if (update instanceof UpdateGroupUserKick) { - UpdateGroupUserKick kick = (UpdateGroupUserKick) update; - users.add(kick.getKickerUid()); - users.add(kick.getUid()); - groups.add(kick.getGroupId()); - } else if (update instanceof UpdateGroupUserLeave) { - UpdateGroupUserLeave leave = (UpdateGroupUserLeave) update; - users.add(leave.getUid()); - groups.add(leave.getGroupId()); } else if (update instanceof UpdateContactsAdded) { users.addAll(((UpdateContactsAdded) update).getUids()); } else if (update instanceof UpdateContactsRemoved) { @@ -80,8 +67,44 @@ public boolean isCausesInvalidation(Update update) { } } } + } else if (update instanceof UpdateGroupMemberChanged) { + UpdateGroupMemberChanged memberChanged = (UpdateGroupMemberChanged) update; + groups.add(memberChanged.getGroupId()); + } else if (update instanceof UpdateGroupMemberDiff) { + UpdateGroupMemberDiff diff = (UpdateGroupMemberDiff) update; + groups.add(diff.getGroupId()); + for (Integer u : diff.getRemovedUsers()) { + users.add(u); + } + for (ApiMember m : diff.getAddedMembers()) { + users.add(m.getInviterUid()); + users.add(m.getUid()); + } + } else if (update instanceof UpdateGroupMembersUpdated) { + UpdateGroupMembersUpdated u = (UpdateGroupMembersUpdated) update; + groups.add(u.getGroupId()); + for (ApiMember m : u.getMembers()) { + users.add(m.getInviterUid()); + users.add(m.getUid()); + } + } else if (update instanceof UpdateGroupMemberAdminChanged) { + UpdateGroupMemberAdminChanged u = (UpdateGroupMemberAdminChanged) update; + users.add(u.getUserId()); + groups.add(u.getGroupId()); + } else if (update instanceof UpdateGroupMembersCountChanged) { + UpdateGroupMembersCountChanged countChanged = (UpdateGroupMembersCountChanged) update; + groups.add(countChanged.getGroupId()); + } else if (update instanceof UpdateGroupOwnerChanged) { + UpdateGroupOwnerChanged ownerChanged = (UpdateGroupOwnerChanged) update; + groups.add(ownerChanged.getGroupId()); + users.add(ownerChanged.getUserId()); + } else if (update instanceof UpdateGroupFullExtChanged) { + UpdateGroupFullExtChanged fullExtChanged = (UpdateGroupFullExtChanged) update; + groups.add(fullExtChanged.getGroupId()); + } else if (update instanceof UpdateGroupExtChanged) { + UpdateGroupExtChanged extChanged = (UpdateGroupExtChanged) update; + groups.add(extChanged.getGroupId()); } - if (!hasUsers(users)) { return true; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/settings/SettingsModule.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/settings/SettingsModule.java index 5ba278b548..b4b527620b 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/settings/SettingsModule.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/settings/SettingsModule.java @@ -43,6 +43,12 @@ public class SettingsModule extends AbsModule { private final String KEY_ANIMATION_AUTO_PLAY; + private final String KEY_DOC_AUTO_DOWNLOAD; + private final String KEY_IMAGE_AUTO_DOWNLOAD; + private final String KEY_VIDEO_AUTO_DOWNLOAD; + private final String KEY_ANIMATION_AUTO_DOWNLOAD; + private final String KEY_AUDIO_AUTO_DOWNLOAD; + private final String KEY_NOTIFICATION_PEER_SOUND; private ActorRef settingsSync; @@ -107,6 +113,12 @@ public SettingsModule(ModuleContext context) { KEY_ANIMATION_AUTO_PLAY = "category." + deviceType + ".auto_play.enabled"; + KEY_ANIMATION_AUTO_DOWNLOAD = "category." + deviceType + ".auto_download_animation.enabled"; + KEY_VIDEO_AUTO_DOWNLOAD = "category." + deviceType + ".auto_download_video.enabled"; + KEY_IMAGE_AUTO_DOWNLOAD = "category." + deviceType + ".auto_download_image.enabled"; + KEY_AUDIO_AUTO_DOWNLOAD = "category." + deviceType + ".auto_download_audio.enabled"; + KEY_DOC_AUTO_DOWNLOAD = "category." + deviceType + ".auto_download_doc.enabled"; + // Account-wide notification settings KEY_NOTIFICATION_SOUND = "account.notification.sound"; KEY_NOTIFICATION_GROUP_ENABLED = "account.notifications.group.enabled"; @@ -255,6 +267,47 @@ public void changeTextSize(int textSize) { setInt(KEY_CHAT_TEXT_SIZE, textSize); } + // Auto download settings + + public boolean isImageAutoDownloadEnabled() { + return getBooleanValue(KEY_IMAGE_AUTO_DOWNLOAD, true); + } + + public void setImageAutoDownloadEnabled(boolean enabled) { + setBooleanValue(KEY_IMAGE_AUTO_DOWNLOAD, enabled); + } + + public boolean isAnimationAutoDownloadEnabled() { + return getBooleanValue(KEY_ANIMATION_AUTO_DOWNLOAD, true); + } + + public void setAnimationAutoDownloadEnabled(boolean enabled) { + setBooleanValue(KEY_ANIMATION_AUTO_DOWNLOAD, enabled); + } + + public boolean isVideoAutoDownloadEnabled() { + return getBooleanValue(KEY_VIDEO_AUTO_DOWNLOAD, false); + } + + public void setVideoAutoDownloadEnabled(boolean enabled) { + setBooleanValue(KEY_VIDEO_AUTO_DOWNLOAD, enabled); + } + + public boolean isDocAutoDownloadEnabled() { + return getBooleanValue(KEY_DOC_AUTO_DOWNLOAD, true); + } + + public void setDocAutoDownloadEnabled(boolean enabled) { + setBooleanValue(KEY_DOC_AUTO_DOWNLOAD, enabled); + } + + public boolean isAudioAutoDownloadEnabled() { + return getBooleanValue(KEY_AUDIO_AUTO_DOWNLOAD, true); + } + + public void setAudioAutoDownloadEnabled(boolean enabled) { + setBooleanValue(KEY_AUDIO_AUTO_DOWNLOAD, enabled); + } // Peer settings @@ -392,6 +445,7 @@ private void changeValue(String key, String val) { } settingsSync.send(new SettingsSyncActor.ChangeSettings(key, val)); onUpdatedSetting(key, val); + notifySettingsChanged(); } private String readValue(String key) { @@ -400,6 +454,9 @@ private String readValue(String key) { public void onUpdatedSetting(String key, String value) { preferences().putString(STORAGE_PREFIX + key, value); + } + + public void notifySettingsChanged() { eventBus.post(new SettingsChanged()); } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/settings/SettingsProcessor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/settings/SettingsProcessor.java index 3ed4418748..be0abcca5a 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/settings/SettingsProcessor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/settings/SettingsProcessor.java @@ -20,6 +20,7 @@ public SettingsProcessor(ModuleContext modules) { public void onSettingsChanged(String key, String value) { context().getSettingsModule().onUpdatedSetting(key, value); + context().getSettingsModule().notifySettingsChanged(); } @Override diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/settings/SettingsSyncActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/settings/SettingsSyncActor.java index 280e739113..8cf212d248 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/settings/SettingsSyncActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/settings/SettingsSyncActor.java @@ -56,7 +56,9 @@ public void onResult(ResponseGetParameters response) { for (ApiParameter p : response.getParameters()) { context().getSettingsModule().onUpdatedSetting(p.getKey(), p.getValue()); } + context().getSettingsModule().notifySettingsChanged(); preferences().putBool(SYNC_STATE_LOADED, true); + context().getConductor().getConductor().onSettingsLoaded(); } @Override @@ -64,6 +66,8 @@ public void onError(RpcException e) { // Ignore } }); + } else { + context().getConductor().getConductor().onSettingsLoaded(); } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/storage/StorageModule.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/storage/StorageModule.java index 405509ee39..79ec3ee010 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/storage/StorageModule.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/storage/StorageModule.java @@ -12,8 +12,8 @@ public class StorageModule extends AbsModule { private static String TAG = "StorageModule"; - private static final int STORAGE_SCHEME_VERSION = 13; - private static final String STORAGE_SCHEME_VERSION_KEY = "storage_sheme_version"; + private static final int STORAGE_SCHEME_VERSION = 14; + private static final String STORAGE_SCHEME_VERSION_KEY = "storage_scheme_version"; private KeyValueStorage storage; @@ -45,6 +45,7 @@ private void performUpgrade(boolean isFirst) { AuthKeyStorage storage = context().getActorApi().getKeyStorage(); long authKey = storage.getAuthKey(); byte[] masterKey = storage.getAuthMasterKey(); + byte[] endpoints = preferences().getBytes("custom_endpoints"); AuthenticationBackupData authenticationBackupData = null; if (!isFirst) { authenticationBackupData = context().getAuthModule().performBackup(); @@ -73,5 +74,9 @@ private void performUpgrade(boolean isFirst) { if (authenticationBackupData != null) { context().getAuthModule().restoreBackup(authenticationBackupData); } + + if (endpoints != null) { + preferences().putBytes("custom_endpoints", endpoints); + } } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/users/router/UserRouter.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/users/router/UserRouter.java index 6ac30a9e19..b54b8085cb 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/users/router/UserRouter.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/modules/users/router/UserRouter.java @@ -26,19 +26,32 @@ import im.actor.core.api.updates.UpdateUserPreferredLanguagesChanged; import im.actor.core.api.updates.UpdateUserTimeZoneChanged; import im.actor.core.api.updates.UpdateUserUnblocked; +import im.actor.core.entity.ContactRecord; +import im.actor.core.entity.ContactRecordType; import im.actor.core.entity.Message; import im.actor.core.entity.MessageState; import im.actor.core.entity.Peer; +import im.actor.core.entity.PhoneBookContact; +import im.actor.core.entity.PhoneBookEmail; +import im.actor.core.entity.PhoneBookPhone; import im.actor.core.entity.User; import im.actor.core.entity.content.ServiceUserRegistered; import im.actor.core.modules.ModuleActor; import im.actor.core.modules.ModuleContext; import im.actor.core.modules.contacts.ContactsSyncActor; +import im.actor.core.modules.contacts.entity.BookImportStorage; import im.actor.core.modules.users.router.entity.RouterApplyUsers; import im.actor.core.modules.users.router.entity.RouterFetchMissingUsers; import im.actor.core.modules.users.router.entity.RouterLoadFullUser; import im.actor.core.modules.users.router.entity.RouterUserUpdate; import im.actor.core.network.parser.Update; +import im.actor.core.providers.PhoneBookProvider; +import im.actor.core.viewmodel.UserEmail; +import im.actor.core.viewmodel.UserPhone; +import im.actor.core.viewmodel.UserVM; +import im.actor.core.viewmodel.generics.ArrayListUserEmail; +import im.actor.core.viewmodel.generics.ArrayListUserPhone; +import im.actor.runtime.Log; import im.actor.runtime.actors.messages.Void; import im.actor.runtime.annotations.Verified; import im.actor.runtime.function.Function; @@ -56,11 +69,13 @@ public class UserRouter extends ModuleActor { private HashSet requestedFullUsers = new HashSet<>(); private boolean isFreezed = false; + PhoneBookProvider phoneBookProvider = config().getPhoneBookProvider(); + List contacts = null; + public UserRouter(ModuleContext context) { super(context); } - // // Small User // @@ -360,6 +375,7 @@ private void onLoadFullUser(int uid) { freeze(); users().getValueAsync(uid) + // Do not reduce to lambda due j2objc bug .flatMap((Function>>) u -> { if (!u.isHaveExtension()) { ArrayList users = new ArrayList<>(); @@ -368,7 +384,18 @@ private void onLoadFullUser(int uid) { .map(responseLoadFullUsers -> new Tuple2<>(responseLoadFullUsers, u)); } else { - return Promise.failure(new RuntimeException("Already loaded")); + //user already loaded, only perform is in phone book check + if (!getUserVM(uid).isInPhoneBook().get()) { + return checkIsInPhoneBook(u).flatMap(new Function>>() { + @Override + public Promise> apply(Void aVoid) { + return Promise.failure(new RuntimeException("Already loaded")); + } + }); + } else { + return Promise.failure(new RuntimeException("Already loaded")); + } + } }) .then(r -> { @@ -379,6 +406,7 @@ private void onLoadFullUser(int uid) { // Updating user in collection users().addOrUpdateItem(upd); }) + .chain(r -> checkIsInPhoneBook(r.getT2().updateExt(r.getT1().getFullUsers().get(0)))) .after((r, e) -> unfreeze()); } @@ -386,8 +414,13 @@ private void onLoadFullUser(int uid) { private Promise> fetchMissingUsers(List users) { freeze(); return PromisesArray.of(users) - .map((Function>) u -> users().containsAsync(u.getUid()) - .map(v -> v ? null : u)) + // Do not reduce due j2objc bug + .map(new Function>() { + @Override + public Promise apply(ApiUserOutPeer u) { + return users().containsAsync(u.getUid()).map(v -> v ? null : u); + } + }) .filterNull() .zip() .after((r, e) -> unfreeze()); @@ -398,8 +431,13 @@ private Promise> fetchMissingUsers(List use private Promise applyUsers(List users) { freeze(); return PromisesArray.of(users) - .map((Function>>) u -> users().containsAsync(u.getId()) - .map(v -> new Tuple2<>(u, v))) + // Do not reduce due j2objc bug + .map(new Function>>() { + @Override + public Promise> apply(ApiUser u) { + return users().containsAsync(u.getId()).map(v -> new Tuple2<>(u, v)); + } + }) .filter(t -> !t.getT2()) .zip() .then(x -> { @@ -415,6 +453,80 @@ private Promise applyUsers(List users) { .after((r, e) -> unfreeze()); } + @Verified + private Promise> getPhoneBook() { + if (contacts == null) { + return new Promise>(resolver -> { + phoneBookProvider.loadPhoneBook(contacts1 -> { + contacts = contacts1; + resolver.result(contacts1); + }); + }); + } else { + return Promise.success(contacts); + } + } + + @Verified + protected Promise checkIsInPhoneBook(User user) { + + if (!config().isEnableOnClientPrivacy()) { + return Promise.success(null); + } + + Log.d("ON_CLIENT_PRIVACY", "checking " + user.getName() + " is in phone book"); + + return getPhoneBook().flatMap(new Function, Promise>() { + @Override + public Promise apply(List phoneBookContacts) { + return new Promise(resolver -> { + List userRecords = user.getRecords(); + + Log.d("ON_CLIENT_PRIVACY", "phonebook have " + phoneBookContacts.size() + " records"); + Log.d("ON_CLIENT_PRIVACY", "user have " + userRecords.size() + " records"); + + outer: + for (ContactRecord record : userRecords) { + + for (PhoneBookContact phoneBookContact : phoneBookContacts) { + + for (PhoneBookPhone phone1 : phoneBookContact.getPhones()) { + if (record.getRecordType() == ContactRecordType.PHONE) { + if (record.getRecordData().equals(phone1.getNumber() + "")) { + context().getContactsModule().markInPhoneBook(user.getUid()); + getUserVM(user.getUid()).isInPhoneBook().change(true); + Log.d("ON_CLIENT_PRIVACY", "in record book!"); + break outer; + } + } + + } + + for (PhoneBookEmail email : phoneBookContact.getEmails()) { + if (record.getRecordType() == ContactRecordType.EMAIL) { + if (record.getRecordData().equals(email.getEmail())) { + context().getContactsModule().markInPhoneBook(user.getUid()); + getUserVM(user.getUid()).isInPhoneBook().change(true); + Log.d("ON_CLIENT_PRIVACY", "in record book!"); + break outer; + } + } + + } + } + + } + + Log.d("ON_CLIENT_PRIVACY", "finish check"); + + + resolver.result(null); + }); + } + }); + + } + // // Tools diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/ActorApi.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/ActorApi.java index 04d082c294..5454113a57 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/ActorApi.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/ActorApi.java @@ -5,11 +5,12 @@ package im.actor.core.network; import im.actor.core.api.ApiVersion; -import im.actor.core.network.parser.ApiParserConfig; +import im.actor.core.network.api.ApiBrokerInt; import im.actor.runtime.actors.ActorRef; import im.actor.core.network.api.ApiBroker; import im.actor.core.network.parser.Request; import im.actor.core.network.parser.Response; +import im.actor.runtime.promise.Promise; import im.actor.runtime.threading.AtomicIntegerCompat; import im.actor.runtime.threading.AtomicLongCompat; @@ -25,7 +26,8 @@ public class ActorApi { private static final AtomicIntegerCompat NEXT_ID = im.actor.runtime.Runtime.createAtomicInt(1); private static final AtomicLongCompat NEXT_RPC_ID = im.actor.runtime.Runtime.createAtomicLong(1); - private final Endpoints endpoints; + private Endpoints endpoints; + private Endpoints defaultEndpoints; private final AuthKeyStorage keyStorage; private final ActorApiCallback callback; private final boolean isEnableLog; @@ -34,6 +36,7 @@ public class ActorApi { private final int maxFailureCount; private ActorRef apiBroker; + private ApiBrokerInt apiBrokerInt; /** * Create API @@ -47,14 +50,16 @@ public ActorApi(Endpoints endpoints, AuthKeyStorage keyStorage, ActorApiCallback int maxDelay, int maxFailureCount) { this.endpoints = endpoints; + this.defaultEndpoints = endpoints; this.keyStorage = keyStorage; this.callback = callback; this.isEnableLog = isEnableLog; this.minDelay = minDelay; this.maxDelay = maxDelay; this.maxFailureCount = maxFailureCount; - this.apiBroker = ApiBroker.get(endpoints, keyStorage, callback, isEnableLog, + this.apiBrokerInt = ApiBroker.get(endpoints, keyStorage, callback, isEnableLog, NEXT_ID.get(), minDelay, maxDelay, maxFailureCount); + this.apiBroker = apiBrokerInt.getDest(); } /** @@ -113,7 +118,27 @@ public synchronized void forceNetworkCheck() { this.apiBroker.send(new ApiBroker.ForceNetworkCheck()); } + /** + * Changing endpoints + */ + public synchronized void changeEndpoints(Endpoints endpoints) { + this.endpoints = endpoints; + this.apiBroker.send(new ApiBroker.ChangeEndpoints(endpoints)); + } + + /** + * Reset default endpoints + */ + public synchronized void resetToDefaultEndpoints() { + this.endpoints = defaultEndpoints; + this.apiBroker.send(new ApiBroker.ChangeEndpoints(endpoints)); + } + public AuthKeyStorage getKeyStorage() { return keyStorage; } + + public Promise checkIsCurrentAuthId(long authId) { + return apiBrokerInt.checkIsCurrentAuthId(authId); + } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/Endpoints.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/Endpoints.java index 8548985441..9d2b086c8b 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/Endpoints.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/Endpoints.java @@ -4,16 +4,26 @@ package im.actor.core.network; +import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import im.actor.runtime.bser.Bser; +import im.actor.runtime.bser.BserObject; +import im.actor.runtime.bser.BserValues; +import im.actor.runtime.bser.BserWriter; import im.actor.runtime.mtproto.ConnectionEndpoint; -public class Endpoints { +public class Endpoints extends BserObject { private int roundRobin = 0; private ConnectionEndpoint[] endpoints; private TrustedKey[] trustedKeys; + public Endpoints() { + } + public Endpoints(ConnectionEndpoint[] endpoints, TrustedKey[] trustedKeys) { this.endpoints = endpoints; this.trustedKeys = trustedKeys; @@ -55,4 +65,39 @@ public ConnectionEndpoint fetchEndpoint(boolean preferEncrypted) { roundRobin = (roundRobin + 1) % endpoints.length; return endpoints[roundRobin]; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Endpoints endpoints1 = (Endpoints) o; + + return Arrays.equals(endpoints, endpoints1.endpoints) && Arrays.equals(trustedKeys, endpoints1.trustedKeys); + } + + public static Endpoints fromBytes(byte[] data) throws IOException { + return Bser.parse(new Endpoints(), data); + } + + @Override + public void parse(BserValues values) throws IOException { + List endpointsRepeatedBytes = values.getRepeatedBytes(1); + endpoints = new ConnectionEndpoint[endpointsRepeatedBytes.size()]; + for (int i = 0; i < endpoints.length; i++) { + endpoints[i] = ConnectionEndpoint.fromBytes(endpointsRepeatedBytes.get(i)); + } + + List trustedKeysRepeatedBytes = values.getRepeatedBytes(2); + trustedKeys = new TrustedKey[trustedKeysRepeatedBytes.size()]; + for (int i = 0; i < trustedKeys.length; i++) { + trustedKeys[i] = TrustedKey.fromBytes(trustedKeysRepeatedBytes.get(i)); + } + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeRepeatedObj(1, new ArrayList<>(Arrays.asList(endpoints))); + writer.writeRepeatedObj(2, new ArrayList<>(Arrays.asList(trustedKeys))); + } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/TrustedKey.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/TrustedKey.java index 5f59451fdd..1251bfda1a 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/TrustedKey.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/TrustedKey.java @@ -1,6 +1,12 @@ package im.actor.core.network; +import java.io.IOException; + import im.actor.runtime.Crypto; +import im.actor.runtime.bser.Bser; +import im.actor.runtime.bser.BserObject; +import im.actor.runtime.bser.BserValues; +import im.actor.runtime.bser.BserWriter; import im.actor.runtime.crypto.Digest; import im.actor.runtime.crypto.primitives.util.ByteStrings; import im.actor.runtime.util.Hex; @@ -11,13 +17,16 @@ #define J2OBJC_DISABLE_ARRAY_BOUND_CHECKS 1 ]-*/ -public class TrustedKey { +public class TrustedKey extends BserObject { private boolean isLoaded = false; - private final String hexKey; + private String hexKey; private long keyId; private byte[] key; + public TrustedKey() { + } + public TrustedKey(String hexKey) { this.hexKey = hexKey; } @@ -45,4 +54,35 @@ private synchronized void load() { this.keyId = ByteStrings.bytesToLong(hash); } } + + public static TrustedKey fromBytes(byte[] data) throws IOException { + return Bser.parse(new TrustedKey(), data); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TrustedKey that = (TrustedKey) o; + + return hexKey.equals(that.hexKey); + + } + + @Override + public void parse(BserValues values) throws IOException { + isLoaded = values.getBool(1); + hexKey = values.getString(2); + keyId = values.getLong(3); + key = values.getBytes(4); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeBool(1, isLoaded); + writer.writeString(2, hexKey); + writer.writeLong(3, keyId); + writer.writeBytes(4, key); + } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/api/ApiBroker.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/api/ApiBroker.java index ed88c1d40e..331028e6e5 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/api/ApiBroker.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/api/ApiBroker.java @@ -14,10 +14,8 @@ import im.actor.core.network.parser.ParsingExtension; import im.actor.runtime.*; import im.actor.runtime.Runtime; -import im.actor.runtime.actors.Actor; import im.actor.runtime.actors.ActorRef; -import im.actor.runtime.actors.ActorSystem; -import im.actor.runtime.actors.Props; +import im.actor.runtime.actors.AskcableActor; import im.actor.core.util.RandomUtils; import im.actor.core.network.mtp.MTProto; import im.actor.core.network.mtp.MTProtoCallback; @@ -32,30 +30,25 @@ import im.actor.core.network.parser.Request; import im.actor.core.network.parser.Response; import im.actor.core.network.parser.RpcScope; +import im.actor.runtime.promise.Promise; import im.actor.runtime.threading.AtomicIntegerCompat; import im.actor.runtime.threading.CommonTimer; -public class ApiBroker extends Actor { +public class ApiBroker extends AskcableActor { - public static ActorRef get(final Endpoints endpoints, final AuthKeyStorage keyStorage, final ActorApiCallback callback, + public static ApiBrokerInt get(final Endpoints endpoints, final AuthKeyStorage keyStorage, final ActorApiCallback callback, final boolean isEnableLog, int id, final int minDelay, final int maxDelay, final int maxFailureCount) { - return ActorSystem.system().actorOf(Props.create(() -> - new ApiBroker(endpoints, - keyStorage, - callback, - isEnableLog, - minDelay, - maxDelay, - maxFailureCount)), "api/broker#" + id); + + return new ApiBrokerInt(endpoints, keyStorage, callback, isEnableLog, id, minDelay, maxDelay, maxFailureCount); } private static final String TAG = "ApiBroker"; private static final AtomicIntegerCompat NEXT_PROTO_ID = im.actor.runtime.Runtime.createAtomicInt(1); - private final Endpoints endpoints; + private Endpoints endpoints; private final AuthKeyStorage keyStorage; private final ActorApiCallback callback; private final boolean isEnableLog; @@ -103,6 +96,14 @@ public void preStart() { } } + public void changeEndpoints(Endpoints endpoints) { + if (endpoints.equals(this.endpoints)) { + return; + } + this.endpoints = endpoints; + recreateAuthId(); + } + @Override public void postStop() { if (proto != null) { @@ -140,13 +141,17 @@ private void onAuthIdInvalidated(long authId) { Log.w(TAG, "Auth id invalidated"); + callback.onAuthIdInvalidated(); + + recreateAuthId(); + } + + private void recreateAuthId() { keyStorage.saveAuthKey(0); keyStorage.saveMasterKey(null); currentAuthId = 0; proto = null; - callback.onAuthIdInvalidated(); - this.keyManager.send(new AuthKeyActor.StartKeyCreation(this.endpoints), self()); } @@ -256,6 +261,11 @@ private void processResponse(long authId, long mid, byte[] content) { response = (Response) parserConfig.parseRpc(ok.responseType, ok.payload); } catch (IOException e) { e.printStackTrace(); + requests.remove(rid); + if (holder.protoId != 0) { + idMap.remove(holder.protoId); + } + holder.callback.onError(new RpcInternalException()); return; } @@ -364,6 +374,10 @@ void connectionCountChanged(int count) { callback.onConnectionsChanged(count); } + private Promise checkIsCurrentAuthId(long authId) { + return new Promise<>(resolver -> resolver.result(authId == currentAuthId)); + } + public static class PerformRequest { private Request message; @@ -432,6 +446,18 @@ public static class ForceNetworkCheck { } + public static class ChangeEndpoints { + Endpoints endpoints; + + public ChangeEndpoints(Endpoints endpoints) { + this.endpoints = endpoints; + } + + public Endpoints getEndpoints() { + return endpoints; + } + } + private class InitMTProto { private long authId; private byte[] authKey; @@ -616,6 +642,14 @@ public void onConnectionsCountChanged(int count) { } } + @Override + public Promise onAsk(Object message) throws Exception { + if (message instanceof CheckIsCurrentAuthId) { + return checkIsCurrentAuthId(((CheckIsCurrentAuthId) message).getAuthId()); + } + return super.onAsk(message); + } + @Override public void onReceive(Object message) { if (message instanceof InitMTProto) { @@ -652,6 +686,8 @@ public void onReceive(Object message) { } else if (message instanceof AuthKeyActor.KeyCreated) { onKeyCreated(((AuthKeyActor.KeyCreated) message).getAuthKeyId(), ((AuthKeyActor.KeyCreated) message).getAuthKey()); + } else if (message instanceof ChangeEndpoints) { + changeEndpoints(((ChangeEndpoints) message).getEndpoints()); } else { super.onReceive(message); } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/api/ApiBrokerInt.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/api/ApiBrokerInt.java new file mode 100644 index 0000000000..b7f9de27ac --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/api/ApiBrokerInt.java @@ -0,0 +1,30 @@ +package im.actor.core.network.api; + +import im.actor.core.network.ActorApiCallback; +import im.actor.core.network.AuthKeyStorage; +import im.actor.core.network.Endpoints; +import im.actor.runtime.actors.ActorInterface; +import im.actor.runtime.promise.Promise; + +import static im.actor.runtime.actors.ActorSystem.system; + +public class ApiBrokerInt extends ActorInterface { + public ApiBrokerInt(final Endpoints endpoints, final AuthKeyStorage keyStorage, final ActorApiCallback callback, + final boolean isEnableLog, int id, final int minDelay, + final int maxDelay, + final int maxFailureCount) { + setDest(system().actorOf("api/broker#" + id, () -> new ApiBroker(endpoints, + keyStorage, + callback, + isEnableLog, + minDelay, + maxDelay, + maxFailureCount))); + } + + public Promise checkIsCurrentAuthId(long authId) { + return ask(new CheckIsCurrentAuthId(authId)); + } + + +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/api/AuthKeyActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/api/AuthKeyActor.java index 080166ad1b..5c9f9ffbac 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/api/AuthKeyActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/api/AuthKeyActor.java @@ -16,7 +16,7 @@ import im.actor.core.network.mtp.entity.ResponseDoDH; import im.actor.core.network.mtp.entity.ResponseGetServerKey; import im.actor.core.network.mtp.entity.ResponseStartAuth; -import im.actor.core.util.ExponentialBackoff; +import im.actor.runtime.util.ExponentialBackoff; import im.actor.runtime.Crypto; import im.actor.runtime.Log; import im.actor.runtime.Network; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/api/CheckIsCurrentAuthId.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/api/CheckIsCurrentAuthId.java new file mode 100644 index 0000000000..960cfae528 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/api/CheckIsCurrentAuthId.java @@ -0,0 +1,15 @@ +package im.actor.core.network.api; + +import im.actor.runtime.actors.ask.AskMessage; + +public class CheckIsCurrentAuthId implements AskMessage { + private long authId; + + public CheckIsCurrentAuthId(long authId) { + this.authId = authId; + } + + public long getAuthId() { + return authId; + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/mtp/actors/ManagerActor.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/mtp/actors/ManagerActor.java index adb270a646..d44a33984f 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/mtp/actors/ManagerActor.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/network/mtp/actors/ManagerActor.java @@ -15,7 +15,7 @@ import im.actor.core.network.mtp.entity.EncryptedCBCPackage; import im.actor.core.network.mtp.entity.EncryptedPackage; import im.actor.core.network.mtp.entity.ProtoMessage; -import im.actor.core.util.ExponentialBackoff; +import im.actor.runtime.util.ExponentialBackoff; import im.actor.runtime.*; import im.actor.runtime.actors.Actor; import im.actor.runtime.actors.ActorRef; diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/util/BitMaskUtil.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/util/BitMaskUtil.java new file mode 100644 index 0000000000..860bd12d26 --- /dev/null +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/util/BitMaskUtil.java @@ -0,0 +1,21 @@ +package im.actor.core.util; + +public class BitMaskUtil { + + public static boolean getBitValue(long src, int index) { + return getBitValue(src, index, false); + } + + public static boolean getBitValue(long src, Enum e) { + return getBitValue(src, e.ordinal(), false); + } + + public static boolean getBitValue(long src, int index, boolean def) { + int val = (int) ((src >> index) & 1); + if (val == 0) { + return def; + } else { + return !def; + } + } +} diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/AppStateVM.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/AppStateVM.java index 2442b066d7..e5a4d84ba4 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/AppStateVM.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/AppStateVM.java @@ -32,6 +32,7 @@ public class AppStateVM { private boolean isBookImported; private boolean isDialogsLoaded; private boolean isContactsLoaded; + private boolean isSettingsLoaded; /** * Constructor of View Model @@ -47,12 +48,13 @@ public AppStateVM(ModuleContext context) { this.isBookImported = context.getPreferences().getBool("app.contacts.imported", false); this.isDialogsLoaded = context.getPreferences().getBool("app.dialogs.loaded", false); this.isContactsLoaded = context.getPreferences().getBool("app.contacts.loaded", false); + this.isSettingsLoaded = context.getPreferences().getBool("app.settings.loaded", false); this.isAppLoaded = new BooleanValueModel("app.loaded", isBookImported && isDialogsLoaded && isContactsLoaded); } private void updateLoaded() { - boolean val = isBookImported && isDialogsLoaded && isContactsLoaded; + boolean val = isBookImported && isDialogsLoaded && isContactsLoaded && isSettingsLoaded; if (isAppLoaded.get() != val) { this.isAppLoaded.change(val); } @@ -128,6 +130,17 @@ public synchronized void onContactsLoaded() { } } + /** + * Notify from Modules about contacts load completed + */ + public synchronized void onSettingsLoaded() { + if (!isSettingsLoaded) { + isSettingsLoaded = true; + context.getPreferences().putBool("app.settings.loaded", true); + updateLoaded(); + } + } + /** * Dialogs empty View Model * @@ -164,4 +177,39 @@ public BooleanValueModel getIsAppEmpty() { return isAppEmpty; } + /** + * Is Phone Book imported + * + * @return is phone book imported state + */ + public boolean isBookImported() { + return isBookImported; + } + + /** + * Is dialogs loaded + * + * @return is dialogs loaded state + */ + public boolean isDialogsLoaded() { + return isDialogsLoaded; + } + + /** + * Is contacts loaded + * + * @return is contacts loaded state + */ + public boolean isContactsLoaded() { + return isContactsLoaded; + } + + /** + * Is settings loaded + * + * @return is contacts loaded state + */ + public boolean isSettingsLoaded() { + return isSettingsLoaded; + } } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/CallVM.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/CallVM.java index be0a615179..3e5a55246e 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/CallVM.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/CallVM.java @@ -35,6 +35,8 @@ public class CallVM { private final BooleanValueModel isAudioEnabled; @Property("nonatomic, readonly") private final BooleanValueModel isVideoEnabled; + @Property("nonatomic, readonly") + private final boolean isVideoPreferred; @Property("nonatomic, readonly") private final ValueModel> members; @@ -45,7 +47,7 @@ public class CallVM { @Property("nonatomic, readonly") private final boolean isOutgoing; - public CallVM(long callId, Peer peer, boolean isOutgoing, boolean isVideoEnabled, ArrayList initialMembers, CallState state) { + public CallVM(long callId, Peer peer, boolean isOutgoing, boolean isVideoEnabled, boolean isVideoPreferred, ArrayList initialMembers, CallState state) { this.callId = callId; this.peer = peer; this.isOutgoing = isOutgoing; @@ -57,6 +59,7 @@ public CallVM(long callId, Peer peer, boolean isOutgoing, boolean isVideoEnabled this.members = new ValueModel<>("calls." + callId + ".members", new ArrayList<>(initialMembers)); this.isAudioEnabled = new BooleanValueModel("calls." + callId + ".audio_enabled", true); this.isVideoEnabled = new BooleanValueModel("calls." + callId + ".video_enabled", isVideoEnabled); + this.isVideoPreferred = isVideoPreferred; this.callStart = 0; } @@ -88,6 +91,10 @@ public ValueModel> getMembers() { return members; } + public boolean isVideoPreferred() { + return isVideoPreferred; + } + public void setCallStart(long callStart) { this.callStart = callStart; } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/ConversationVM.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/ConversationVM.java index 8fb7029df2..1c91885870 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/ConversationVM.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/ConversationVM.java @@ -58,7 +58,7 @@ public ValueModel getReceiveDate() { return receiveDate; } - public long getLastMessageDate() { + public long getLastReadMessageDate() { return Math.max(ownReadDate.get(), ownSendDate.get()); } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/GlobalStateVM.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/GlobalStateVM.java index 55a2e5bc77..91738f99ae 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/GlobalStateVM.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/GlobalStateVM.java @@ -30,25 +30,19 @@ public GlobalStateVM(ModuleContext context) { this.isConnecting = new BooleanValueModel("app.connecting", false); this.isSyncing = new BooleanValueModel("app.syncing", false); - context.getEvents().subscribe(new BusSubscriber() { - @Override - public void onBusEvent(Event event) { - if (event instanceof AppVisibleChanged) { - if (((AppVisibleChanged) event).isVisible()) { - isAppVisible.change(true); - globalTempCounter.change(0); - } else { - isAppVisible.change(false); - } + context.getEvents().subscribe(event -> { + if (event instanceof AppVisibleChanged) { + if (((AppVisibleChanged) event).isVisible()) { + isAppVisible.change(true); + globalTempCounter.change(0); + } else { + isAppVisible.change(false); } } }, AppVisibleChanged.EVENT); - context.getEvents().subscribe(new BusSubscriber() { - @Override - public void onBusEvent(Event event) { - isConnecting.change(((ConnectingStateChanged) event).isConnecting()); - } + context.getEvents().subscribe(event -> { + isConnecting.change(((ConnectingStateChanged) event).isConnecting()); }, ConnectingStateChanged.EVENT); } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/GroupVM.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/GroupVM.java index 0e6ae68e6e..dbffde90c5 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/GroupVM.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/GroupVM.java @@ -8,16 +8,18 @@ import com.google.j2objc.annotations.Property; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import im.actor.core.api.ApiMapValue; import im.actor.core.entity.Group; import im.actor.core.entity.GroupMember; +import im.actor.core.entity.GroupType; import im.actor.core.viewmodel.generics.AvatarValueModel; import im.actor.core.viewmodel.generics.BooleanValueModel; +import im.actor.core.viewmodel.generics.IntValueModel; import im.actor.core.viewmodel.generics.StringValueModel; import im.actor.runtime.annotations.MainThread; import im.actor.runtime.mvvm.BaseValueModel; @@ -30,45 +32,113 @@ */ public class GroupVM extends BaseValueModel { - public static ValueModelCreator CREATOR(final int myUid) { - return new ValueModelCreator() { - @Override - public GroupVM create(Group baseValue) { - return new GroupVM(baseValue, myUid); - } - }; - } + public static ValueModelCreator CREATOR = GroupVM::new; @Property("nonatomic, readonly") private int groupId; - @Property("nonatomic, readonly") - private int creatorId; @NotNull @Property("nonatomic, readonly") - private AvatarValueModel avatar; + private GroupType groupType; @NotNull @Property("nonatomic, readonly") private StringValueModel name; @NotNull @Property("nonatomic, readonly") + private AvatarValueModel avatar; + @NotNull + @Property("nonatomic, readonly") private BooleanValueModel isMember; + @NotNull + @Property("nonatomic, readonly") + private IntValueModel membersCount; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanWriteMessage; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanCall; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanClear; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanJoin; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanViewInfo; + @NotNull @Property("nonatomic, readonly") private ValueModel> members; @NotNull @Property("nonatomic, readonly") - private ValueModel presence; - @Nullable + private BooleanValueModel isAsyncMembers; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanViewMembers; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanInviteMembers; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanEditInfo; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isHistoryShared; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanEditAdministration; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanEditAdmins; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanViewAdmins; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanLeave; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanDelete; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanInviteViaLink; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanKickInvited; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanKickAnyone; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanEditForeign; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isCanDeleteForeign; + @NotNull + @Property("nonatomic, readonly") + private BooleanValueModel isDeleted; + + @NotNull @Property("nonatomic, readonly") private StringValueModel theme; - @Nullable + @NotNull @Property("nonatomic, readonly") private StringValueModel about; - - private int myUid; + @NotNull + @Property("nonatomic, readonly") + private StringValueModel shortName; + @Property("nonatomic, readonly") + private IntValueModel ownerId; + @NotNull + @Property("nonatomic, readonly") + private ValueModel presence; + @NotNull + @Property("nonatomic, readonly") + private ValueModel ext; @NotNull - private ArrayList> listeners = new ArrayList>(); + private ArrayList> listeners = new ArrayList<>(); /** *

INTERNAL API

@@ -76,18 +146,43 @@ public GroupVM create(Group baseValue) { * * @param rawObj initial value of Group */ - public GroupVM(@NotNull Group rawObj, int myUid) { + public GroupVM(@NotNull Group rawObj) { super(rawObj); - this.myUid = myUid; this.groupId = rawObj.getGroupId(); - this.creatorId = rawObj.getCreatorId(); + this.groupType = rawObj.getGroupType(); this.name = new StringValueModel("group." + groupId + ".title", rawObj.getTitle()); this.avatar = new AvatarValueModel("group." + groupId + ".avatar", rawObj.getAvatar()); - this.isMember = new BooleanValueModel("group." + groupId + ".isMember", isHaveMember(myUid, rawObj.getMembers())); - this.members = new ValueModel>("group." + groupId + ".members", new HashSet(rawObj.getMembers())); - this.presence = new ValueModel("group." + groupId + ".presence", 0); - this.theme = new StringValueModel("group." + groupId + ".theme", rawObj.getTheme()); + this.isMember = new BooleanValueModel("group." + groupId + ".isMember", rawObj.isMember()); + this.membersCount = new IntValueModel("group." + groupId + ".membersCount", rawObj.getMembersCount()); + this.isCanWriteMessage = new BooleanValueModel("group." + groupId + ".can_write", rawObj.isCanSendMessage()); + this.isCanCall = new BooleanValueModel("group." + groupId + ".can_call", rawObj.isCanCall()); + this.isCanViewMembers = new BooleanValueModel("group." + groupId + ".can_view_members", rawObj.isCanViewMembers()); + this.isCanInviteMembers = new BooleanValueModel("group." + groupId + ".can_invite_members", rawObj.isCanInviteMembers()); + this.isCanEditInfo = new BooleanValueModel("group." + groupId + ".can_edit_info", rawObj.isCanEditInfo()); + this.isAsyncMembers = new BooleanValueModel("group." + groupId + ".isAsyncMembers", rawObj.isAsyncMembers()); + this.isCanEditAdministration = new BooleanValueModel("group." + groupId + ".isCanEditAdministration", rawObj.isCanEditAdministration()); + this.isHistoryShared = new BooleanValueModel("group." + groupId + ".isHistoryShared", rawObj.isSharedHistory()); + this.isCanEditAdmins = new BooleanValueModel("group." + groupId + ".isCanEditAdmins", rawObj.isCanEditAdmins()); + this.isCanViewAdmins = new BooleanValueModel("group." + groupId + ".isCanViewAdmins", rawObj.isCanViewAdmins()); + this.isCanLeave = new BooleanValueModel("group." + groupId + ".isCanLeave", rawObj.isCanLeave()); + this.isCanDelete = new BooleanValueModel("group." + groupId + ".isCanDelete", rawObj.isCanDelete()); + this.isCanInviteViaLink = new BooleanValueModel("group." + groupId + ".isCanInviteViaLink", rawObj.isCanInviteViaLink()); + this.isCanKickInvited = new BooleanValueModel("group." + groupId + ".isCanKickInvited", rawObj.isCanKickInvited()); + this.isCanKickAnyone = new BooleanValueModel("group." + groupId + ".isCanKickAnyone", rawObj.isCanKickAnyone()); + this.isCanEditForeign = new BooleanValueModel("group." + groupId + ".isCanEditForeign", rawObj.isCanEditForeign()); + this.isCanDeleteForeign = new BooleanValueModel("group." + groupId + ".isCanDeleteForeign", rawObj.isCanDeleteForeign()); + this.isDeleted = new BooleanValueModel("group." + groupId + ".isDeleted", rawObj.isDeleted()); + this.isCanClear = new BooleanValueModel("group." + groupId + ".isCanClear", rawObj.isCanClear()); + this.isCanJoin = new BooleanValueModel("group." + groupId + ".isCanJoin", rawObj.isCanJoin()); + this.isCanViewInfo = new BooleanValueModel("group." + groupId + ".isCanViewInfo", rawObj.isCanViewInfo()); + + this.ownerId = new IntValueModel("group." + groupId + ".membersCount", rawObj.getOwnerId()); + this.members = new ValueModel<>("group." + groupId + ".members", new HashSet<>(rawObj.getMembers())); + this.presence = new ValueModel<>("group." + groupId + ".presence", 0); + this.theme = new StringValueModel("group." + groupId + ".theme", rawObj.getTopic()); this.about = new StringValueModel("group." + groupId + ".about", rawObj.getAbout()); + this.shortName = new StringValueModel("group." + groupId + ".shortname", rawObj.getShortName()); + this.ext = new ValueModel<>("group." + groupId + ".ext", rawObj.getExt()); } /** @@ -101,23 +196,14 @@ public int getId() { } /** - * Get Group creator user id - * - * @return creator user id - */ - @ObjectiveCName("getCreatorId") - public int getCreatorId() { - return creatorId; - } - - /** - * Get Group members count + * Get Group Type * - * @return members count + * @return Group Type */ - @ObjectiveCName("getMembersCount") - public int getMembersCount() { - return members.get().size(); + @NotNull + @ObjectiveCName("getGroupType") + public GroupType getGroupType() { + return groupType; } /** @@ -142,6 +228,39 @@ public AvatarValueModel getAvatar() { return avatar; } + /** + * Get About Value Model + * + * @return Value Model of String + */ + @NotNull + @ObjectiveCName("getAboutModel") + public StringValueModel getAbout() { + return about; + } + + /** + * Get Theme Value Model + * + * @return Value Model of String + */ + @NotNull + @ObjectiveCName("getThemeModel") + public StringValueModel getTheme() { + return theme; + } + + /** + * Get Short Name Model + * + * @return Value Model of String + */ + @NotNull + @ObjectiveCName("getShortNameModel") + public StringValueModel getShortName() { + return shortName; + } + /** * Get membership Value Model * @@ -153,6 +272,246 @@ public BooleanValueModel isMember() { return isMember; } + /** + * Get Group members count + * + * @return members count + */ + @NotNull + @ObjectiveCName("getMembersCountModel") + public IntValueModel getMembersCount() { + return membersCount; + } + + /** + * Can current user write message to a group + * + * @return can write message model + */ + @NotNull + @ObjectiveCName("isCanWriteMessageModel") + public BooleanValueModel getIsCanWriteMessage() { + return isCanWriteMessage; + } + + /** + * Can current user view members of a group + * + * @return can view members model + */ + @NotNull + @ObjectiveCName("getIsCanViewMembersModel") + public BooleanValueModel getIsCanViewMembers() { + return isCanViewMembers; + } + + /** + * Can current user edit group info + * + * @return can edit group info + */ + @NotNull + @ObjectiveCName("isCanEditInfoModel") + public BooleanValueModel getIsCanEditInfo() { + return isCanEditInfo; + } + + /** + * Can current user invite members to a group + * + * @return can invite members model + */ + @NotNull + @ObjectiveCName("getIsCanInviteMembersModel") + public BooleanValueModel getIsCanInviteMembers() { + return isCanInviteMembers; + } + + + /** + * Is members should be fetched async + * + * @return is members async model + */ + @NotNull + @ObjectiveCName("getIsAsyncMembersModel") + public BooleanValueModel getIsAsyncMembers() { + return isAsyncMembers; + } + + /** + * Is history shared in this group + * + * @return is history shared model + */ + @NotNull + @ObjectiveCName("getIsHistorySharedModel") + public BooleanValueModel getIsHistoryShared() { + return isHistoryShared; + } + + /** + * Is current user can edit administration settings + * + * @return is can edit administration model + */ + @NotNull + @ObjectiveCName("getIsCanEditAdministrationModel") + public BooleanValueModel getIsCanEditAdministration() { + return isCanEditAdministration; + } + + /** + * Is current user can leave group + * + * @return is current user can leave model + */ + @NotNull + @ObjectiveCName("getIsCanLeaveModel") + public BooleanValueModel getIsCanLeave() { + return isCanLeave; + } + + /** + * Is current user can delete group + * + * @return is current user can delete model + */ + @NotNull + @ObjectiveCName("getIsCanDeleteModel") + public BooleanValueModel getIsCanDelete() { + return isCanDelete; + } + + /** + * Is current user can call in this group + * + * @return is current user can call model + */ + @NotNull + @ObjectiveCName("getIsCanCallModel") + public BooleanValueModel getIsCanCall() { + return isCanCall; + } + + /** + * Is current user can invite via link + * + * @return is current user can invite via link model + */ + @NotNull + @ObjectiveCName("getIsCanInviteViaLinkModel") + public BooleanValueModel getIsCanInviteViaLink() { + return isCanInviteViaLink; + } + + /** + * Is current user can kick invited members + * + * @return is current user can kick invited model + */ + @NotNull + @ObjectiveCName("getIsCanKickInvitedModel") + public BooleanValueModel getIsCanKickInvited() { + return isCanKickInvited; + } + + /** + * Is current user can kick anyone + * + * @return is current user can kick anyone model + */ + @NotNull + @ObjectiveCName("getIsCanKickAnyoneModel") + public BooleanValueModel getIsCanKickAnyone() { + return isCanKickAnyone; + } + + /** + * Is current user can edit foreign messages + * + * @return is current user can edit foreign messages model + */ + @NotNull + @ObjectiveCName("getIsCanEditForeignModel") + public BooleanValueModel getIsCanEditForeign() { + return isCanEditForeign; + } + + /** + * Is current user can delete foreign messages + * + * @return is current user can delete foreign messages model + */ + @NotNull + @ObjectiveCName("getIsCanDeleteForeignModel") + public BooleanValueModel getIsCanDeleteForeign() { + return isCanDeleteForeign; + } + + /** + * Is current user can clear messages + * + * @return is current user can clear messages model + */ + @NotNull + @ObjectiveCName("getIsCanClearModel") + public BooleanValueModel getIsCanClear() { + return isCanClear; + } + + /** + * Is current user can view info + * + * @return is current user can view info model + */ + @NotNull + @ObjectiveCName("getIsCanViewInfoModel") + public BooleanValueModel getIsCanViewInfo() { + return isCanViewInfo; + } + + /** + * Is current user can join + * + * @return is current user can join model + */ + @NotNull + public BooleanValueModel getIsCanJoin() { + return isCanJoin; + } + + /** + * Is current user can edit admins + * + * @return is current user can edit admins + */ + @NotNull + public BooleanValueModel getIsCanEditAdmins() { + return isCanEditAdmins; + } + + /** + * Is group deleted + * + * @return is this group deleted model + */ + @NotNull + @ObjectiveCName("getIsDeletedModel") + public BooleanValueModel getIsDeleted() { + return isDeleted; + } + + /** + * Get Group owner user id model + * + * @return creator owner id model + */ + @ObjectiveCName("getCreatorIdModel") + public IntValueModel getOwnerId() { + return ownerId; + } + /** * Get members Value Model * @@ -175,40 +534,14 @@ public ValueModel getPresence() { return presence; } - @Override - protected void updateValues(@NotNull Group rawObj) { - boolean isChanged = name.change(rawObj.getTitle()); - isChanged |= theme.change(rawObj.getTheme()); - isChanged |= about.change(rawObj.getAbout()); - isChanged |= avatar.change(rawObj.getAvatar()); - isChanged |= isMember.change(isHaveMember(myUid, rawObj.getMembers())); - isChanged |= members.change(new HashSet(rawObj.getMembers())); - - if (isChanged) { - notifyChange(); - } - } - - /** - * Get About Value Model - * - * @return Value Model of String - */ - @Nullable - @ObjectiveCName("getAboutModel") - public StringValueModel getAbout() { - return about; - } - /** - * Get Theme Value Model + * Get ext Value Model * - * @return Value Model of String + * @return Value Model of ext */ - @Nullable - @ObjectiveCName("getThemeModel") - public StringValueModel getTheme() { - return theme; + @NotNull + public ValueModel getExt() { + return ext; } /** @@ -218,8 +551,7 @@ public StringValueModel getTheme() { */ @MainThread @ObjectiveCName("subscribeWithListener:") - public void subscribe(@NotNull ModelChangedListener listener) { - im.actor.runtime.Runtime.checkMainThread(); + public synchronized void subscribe(@NotNull ModelChangedListener listener) { if (listeners.contains(listener)) { return; } @@ -234,8 +566,7 @@ public void subscribe(@NotNull ModelChangedListener listener) { */ @MainThread @ObjectiveCName("subscribeWithListener:withNotify:") - public void subscribe(@NotNull ModelChangedListener listener, boolean notify) { - im.actor.runtime.Runtime.checkMainThread(); + public synchronized void subscribe(@NotNull ModelChangedListener listener, boolean notify) { if (listeners.contains(listener)) { return; } @@ -252,28 +583,66 @@ public void subscribe(@NotNull ModelChangedListener listener, boolean n */ @MainThread @ObjectiveCName("unsubscribeWithListener:") - public void unsubscribe(@NotNull ModelChangedListener listener) { - im.actor.runtime.Runtime.checkMainThread(); + public synchronized void unsubscribe(@NotNull ModelChangedListener listener) { listeners.remove(listener); } - private void notifyChange() { - im.actor.runtime.Runtime.postToMainThread(new Runnable() { - @Override - public void run() { - for (ModelChangedListener l : listeners.toArray(new ModelChangedListener[listeners.size()])) { - l.onChanged(GroupVM.this); - } - } - }); + // + // Update handling + // + + @Override + protected void updateValues(@NotNull Group rawObj) { + boolean isChanged = name.change(rawObj.getTitle()); + + isChanged |= avatar.change(rawObj.getAvatar()); + isChanged |= membersCount.change(rawObj.getMembersCount()); + isChanged |= isMember.change(rawObj.isMember()); + isChanged |= isCanWriteMessage.change(rawObj.isCanSendMessage()); + + isChanged |= theme.change(rawObj.getTopic()); + isChanged |= about.change(rawObj.getAbout()); + isChanged |= members.change(new HashSet<>(rawObj.getMembers())); + isChanged |= ownerId.change(rawObj.getOwnerId()); + isChanged |= isCanViewMembers.change(rawObj.isCanViewMembers()); + isChanged |= isCanInviteMembers.change(rawObj.isCanInviteMembers()); + isChanged |= isCanEditInfo.change(rawObj.isCanEditInfo()); + isChanged |= shortName.change(rawObj.getShortName()); + isChanged |= isAsyncMembers.change(rawObj.isAsyncMembers()); + isChanged |= isHistoryShared.change(rawObj.isSharedHistory()); + isChanged |= isCanEditAdministration.change(rawObj.isCanEditAdministration()); + isChanged |= isCanEditAdmins.change(rawObj.isCanEditAdmins()); + isChanged |= isCanViewAdmins.change(rawObj.isCanViewAdmins()); + isChanged |= isCanLeave.change(rawObj.isCanLeave()); + isChanged |= isCanDelete.change(rawObj.isCanDelete()); + isChanged |= isCanInviteViaLink.change(rawObj.isCanInviteViaLink()); + isChanged |= isCanKickInvited.change(rawObj.isCanKickInvited()); + isChanged |= isCanKickAnyone.change(rawObj.isCanKickAnyone()); + isChanged |= isCanEditForeign.change(rawObj.isCanEditForeign()); + isChanged |= isCanDeleteForeign.change(rawObj.isCanDeleteForeign()); + isChanged |= isDeleted.change(rawObj.isDeleted()); + isChanged |= isCanClear.change(rawObj.isCanClear()); + isChanged |= isCanViewInfo.change(rawObj.isCanViewInfo()); + isChanged |= isCanJoin.change(rawObj.isCanJoin()); + isChanged |= isCanCall.change(rawObj.isCanCall()); + isChanged |= ext.change(rawObj.getExt()); + + if (isChanged) { + notifyIfNeeded(); + } } - private boolean isHaveMember(int uid, Collection members) { - for (GroupMember m : members) { - if (m.getUid() == uid) { - return true; - } + private synchronized void notifyIfNeeded() { + if (listeners.size() > 0) { + notifyChange(); } - return false; + } + + private void notifyChange() { + im.actor.runtime.Runtime.postToMainThread(() -> { + for (ModelChangedListener l : listeners) { + l.onChanged(GroupVM.this); + } + }); } } \ No newline at end of file diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/UserVM.java b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/UserVM.java index 83c4a02d9b..8752651a88 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/UserVM.java +++ b/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/viewmodel/UserVM.java @@ -69,6 +69,8 @@ public static ValueModelCreator CREATOR(final ModuleContext module @NotNull private BooleanValueModel isContact; @NotNull + private BooleanValueModel isInPhoneBook; + @NotNull private BooleanValueModel isBlocked; @NotNull private BooleanValueModel isVerified; @@ -112,6 +114,7 @@ public UserVM(@NotNull User user, @NotNull ModuleContext modules) { about = new StringValueModel("user." + id + ".about", user.getAbout()); avatar = new AvatarValueModel("user." + id + ".avatar", user.getAvatar()); isContact = new BooleanValueModel("user." + id + ".contact", modules.getContactsModule().isUserContact(id)); + isInPhoneBook = new BooleanValueModel("user." + id + ".in_pb", modules.getContactsModule().isUserInPhoneBook(id)); isBlocked = new BooleanValueModel("user." + id + ".blocked", user.isBlocked()); isVerified = new BooleanValueModel("user." + id + ".is_verified", user.isVerified()); timeZone = new StringValueModel("user." + id + ".time_zone", user.getTimeZone()); @@ -264,6 +267,17 @@ public BooleanValueModel isContact() { return isContact; } + /** + * Get ValueModel of flag if user is in phone book + * + * @return ValueModel of Boolean + */ + @NotNull + @ObjectiveCName("isInPhoneBookModel") + public BooleanValueModel isInPhoneBook() { + return isInPhoneBook; + } + /** * Get ValueModel of flag if user is blocked * @@ -372,7 +386,7 @@ public ValueModelBotCommands getBotCommands() { @MainThread @ObjectiveCName("subscribeWithListener:") public void subscribe(@NotNull ModelChangedListener listener) { - Runtime.checkMainThread(); + // Runtime.checkMainThread(); if (listeners.contains(listener)) { return; } @@ -388,7 +402,7 @@ public void subscribe(@NotNull ModelChangedListener listener) { @MainThread @ObjectiveCName("subscribeWithListener:withNotify:") public void subscribe(@NotNull ModelChangedListener listener, boolean notify) { - Runtime.checkMainThread(); + // Runtime.checkMainThread(); if (listeners.contains(listener)) { return; } @@ -406,17 +420,14 @@ public void subscribe(@NotNull ModelChangedListener listener, boolean no @MainThread @ObjectiveCName("unsubscribeWithListener:") public void unsubscribe(@NotNull ModelChangedListener listener) { - Runtime.checkMainThread(); + // Runtime.checkMainThread(); listeners.remove(listener); } private void notifyChange() { - Runtime.postToMainThread(new Runnable() { - @Override - public void run() { - for (ModelChangedListener l : listeners.toArray(new ModelChangedListener[listeners.size()])) { - l.onChanged(UserVM.this); - } + Runtime.postToMainThread(() -> { + for (ModelChangedListener l : listeners) { + l.onChanged(UserVM.this); } }); } diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText.json b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText.json index 78f9d1a313..53778a8d92 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText.json +++ b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText.json @@ -107,6 +107,27 @@ "avatar_changed": "{name} changed the group photo", "avatar_removed": "{name} removed the group photo" }, + "channels": { + "created": "Channel created", + "invited": "{name} invited {name_added}", + "joined": "{name} joined channel", + "kicked": "{name} kicked {name_kicked}", + "left": "{name} left channel", + "title_changed": { + "full": "{name} changed the channel name to \"{title}\"", + "compact": "{name} changed the channel name" + }, + "topic_changed": { + "full": "{name} changed the channel topic to \"{topic}\"", + "compact": "{name} changed the channel topic" + }, + "about_changed": { + "full": "{name} changed the channel about to \"{about}\"", + "compact": "{name} changed the channel about" + }, + "avatar_changed": "{name} changed the channel photo", + "avatar_removed": "{name} removed the channel photo" + }, "calls": { "missed": "Missed call", "ended": "Call ended" diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Ar.json b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Ar.json index 68524a5980..308c7b5142 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Ar.json +++ b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Ar.json @@ -94,6 +94,27 @@ "avatar_changed": "{name} غير صورة المجموعة", "avatar_removed": "{name} حذف صورة المجموعة" }, + "channels": { + "created": " انشأ المجموعة", + "invited": "{name} دعى {name_added}", + "joined": "{name} انصم للمجموعة", + "kicked": "{name} ازال {name_kicked}", + "left": "{name} غادر", + "title_changed": { + "full": "{name} غير الاسم الى \"{title}\"", + "compact": "{name} غير اسم المجموعة الى" + }, + "topic_changed": { + "full": "{name} changed the group topic to \"{topic}\"", + "compact": "{name} changed the group topic" + }, + "about_changed": { + "full": "{name} changed the group about to \"{about}\"", + "compact": "{name} changed the group about" + }, + "avatar_changed": "{name} غير صورة المجموعة", + "avatar_removed": "{name} حذف صورة المجموعة" + }, "calls": { "missed": "Missed call", "ended": "Call ended" diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Es.json b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Es.json index d3e2e67c25..7cdcd77f9d 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Es.json +++ b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Es.json @@ -100,6 +100,27 @@ "avatar_changed": "{name} modificada la foto de grupo", "avatar_removed": "{name} eliminada la foto de grupo" }, + "channels": { + "created": "Creado el canal", + "invited": "{name} invitó a {name_added}", + "joined": "{name} se unió al canal", + "kicked": "{name} expulsaste a {name_kicked}", + "left": "{name} elimino el canal", + "title_changed": { + "full": "{name} cambio el nombre del canal a \"{title}\"", + "compact": "{name} a modificado el nombre del canal" + }, + "topic_changed": { + "full": "{name} changed the canal topic to \"{topic}\"", + "compact": "{name} changed the canal topic" + }, + "about_changed": { + "full": "{name} changed the canal about to \"{about}\"", + "compact": "{name} changed the canal about" + }, + "avatar_changed": "{name} modificada la foto de canal", + "avatar_removed": "{name} eliminada la foto de canal" + }, "calls": { "missed": "Llamada perdida", "ended": "Llamada finalizada" diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Fa.json b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Fa.json index 0a6b3873b2..0d663fece5 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Fa.json +++ b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Fa.json @@ -94,6 +94,27 @@ "avatar_changed": "{name} تصویر گروه را عوض کرد", "avatar_removed": "{name} تصویر گروه را عوض کرد" }, + "channels": { + "created": " گروه را ایجاد کرد", + "invited": "{name} {name_added} را دعوت کرد", + "joined": "{name} به گروه پیوست", + "kicked": "{name} {name_kicked} را از گروه حذف کرد", + "left": "{name} از گروه رفت", + "title_changed": { + "full": "{name} نام گروه را با\"{title}\" جایگزین کرد", + "compact": "{name} نام گروه را جایگزین کرد" + }, + "topic_changed": { + "full": "جناب {name} موضوع گروه را با \"{topic}\" جای‌گزین کردید", + "compact": "موضوع گروه را عوض کرد {name}" + }, + "about_changed": { + "full": "جناب {name} متن درباره گروه را با \"{about}\" جای‌گزین کردید", + "compact": "متن درباره گروه را عوض کرد {name}" + }, + "avatar_changed": "{name} تصویر گروه را عوض کرد", + "avatar_removed": "{name} تصویر گروه را عوض کرد" + }, "calls": { "missed": "تماس از دست رفته", "ended": "تماس تمام شد" diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Pt.json b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Pt.json index b2704c9990..f42f5e152e 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Pt.json +++ b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Pt.json @@ -107,6 +107,27 @@ "avatar_changed": "{name} alterou a foto do grupo", "avatar_removed": "{name} removeu a foto do grupo" }, + "channels": { + "created": "Criou o canal", + "invited": "{name} convidado {name_added}", + "joined": "{name} entrou no canal", + "kicked": "{name} removido {name_kicked}", + "left": "{name} saiu do canal", + "title_changed": { + "full": "{name} alterou o nome do canal para \"{title}\"", + "compact": "{name} alterou o nome do canal" + }, + "topic_changed": { + "full": "{name} changed the canal topic to \"{topic}\"", + "compact": "{name} changed the canal topic" + }, + "about_changed": { + "full": "{name} changed the canal about to \"{about}\"", + "compact": "{name} changed the canal about" + }, + "avatar_changed": "{name} alterou a foto do canal", + "avatar_removed": "{name} removeu a foto do canal" + }, "calls": { "missed": "Missed call", "ended": "Call ended" diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Ru.json b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Ru.json index b4793ec1b4..0f8bc7a55e 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Ru.json +++ b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Ru.json @@ -160,10 +160,10 @@ "other": "{name} изменил(-а) название группы на \"{title}\"" }, "compact": { - "you": "{name} изменили тему группы", - "male": "{name} изменил тему группы", - "female": "{name} изменила тему группы", - "other": "{name} изменил(-а) тему группы" + "you": "{name} изменили название группы", + "male": "{name} изменил название группы", + "female": "{name} изменила название группы", + "other": "{name} изменил(-а) название группы" } }, "topic_changed": { @@ -207,6 +207,87 @@ "other": "{name} удалил(-а) фото группы" } }, + "channels": { + "created": "Создан канал", + "invited": { + "you": "{name} пригласили {name_added}", + "male": "{name} пригласил {name_added}", + "female": "{name} пригласила {name_added}", + "other": "{name} пригласил(-а) {name_added}" + }, + "joined": { + "you": "{name} вошли в канал", + "male": "{name} вошел в канал", + "female": "{name} вошла в канал", + "other": "{name} вошел(-шла) в канал" + }, + "kicked": { + "you": "{name} исключили {name_kicked}", + "male": "{name} исключил {name_kicked}", + "female": "{name} исключила {name_kicked}", + "other": "{name} исключил(-а) {name_kicked}" + }, + "left": { + "you": "{name} покинули канал", + "male": "{name} покинул канал", + "female": "{name} покинула канал", + "other": "{name} покинул(-а) канал" + }, + "title_changed": { + "full": { + "you": "{name} изменили название канала на \"{title}\"", + "male": "{name} изменил название канала на \"{title}\"", + "female": "{name} изменила название канала на \"{title}\"", + "other": "{name} изменил(-а) название канала на \"{title}\"" + }, + "compact": { + "you": "{name} изменили название канала", + "male": "{name} изменил название канала", + "female": "{name} изменила название канала", + "other": "{name} изменил(-а) название канала" + } + }, + "topic_changed": { + "full": { + "you": "{name} изменили тему канала на \"{title}\"", + "male": "{name} изменил тему канала на \"{title}\"", + "female": "{name} изменила тему канала на \"{title}\"", + "other": "{name} изменил(-а) тему канала на \"{title}\"" + }, + "compact": { + "you": "{name} изменили тему канала", + "male": "{name} изменил тему канала", + "female": "{name} изменила тему канала", + "other": "{name} изменил(-а) тему канала" + } + }, + "about_changed": { + "full": { + "you": "{name} изменили описание канала на \"{title}\"", + "male": "{name} изменил описание канала на \"{title}\"", + "female": "{name} изменила описание канала на \"{title}\"", + "other": "{name} изменил(-а) описание канала на \"{title}\"" + }, + "compact": { + "you": "{name} изменили канала канала", + "male": "{name} изменил описание канала", + "female": "{name} изменила описание канала", + "other": "{name} изменил(-а) описание канала" + } + }, + "avatar_changed": { + "you": "{name} изменили фото канала", + "male": "{name} изменил фото канала", + "female": "{name} изменила фото канала", + "other": "{name} изменил(-а) фото канала" + }, + "avatar_removed": { + "you": "{name} удалили фото канала", + "male": "{name} удалил фото канала", + "female": "{name} удалила фото канала", + "other": "{name} удалил(-а) фото канала" + } + }, "calls": { "missed": "Пропущен вызов", "ended": "Вызов завершен" diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Zn.json b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Zn.json index 38a30bf975..f253e8b0ee 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Zn.json +++ b/actor-sdk/sdk-core/core/core-shared/src/main/resources/AppText_Zn.json @@ -101,6 +101,27 @@ "avatar_changed": "{name}修改了群组头像", "avatar_removed": "{name}删除了群组头像" }, + "channels": { + "created": "{name}创建了这个群组", + "invited": "{name}邀请{name_added}", + "joined": "{name}加入群组", + "kicked": "{name}移出{name_kicked}", + "left": "{name}退出群组", + "title_changed": { + "full": "{name}修改为名称为:\"{title}\"", + "compact": "{name}修改了群组名称" + }, + "topic_changed": { + "full": "{name} changed the group topic to \"{topic}\"", + "compact": "{name} changed the group topic" + }, + "about_changed": { + "full": "{name} changed the group about to \"{about}\"", + "compact": "{name} changed the group about" + }, + "avatar_changed": "{name}修改了群组头像", + "avatar_removed": "{name}删除了群组头像" + }, "calls": { "missed": "未接电话", "ended": "拨号结束" diff --git a/actor-sdk/sdk-core/runtime/runtime-android/build.gradle b/actor-sdk/sdk-core/runtime/runtime-android/build.gradle index 519b7d21a8..ef3cd88fdd 100644 --- a/actor-sdk/sdk-core/runtime/runtime-android/build.gradle +++ b/actor-sdk/sdk-core/runtime/runtime-android/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:2.1.2' + classpath 'com.android.tools.build:gradle:2.1.3' classpath 'me.tatarka:gradle-retrolambda:3.2.5' } } diff --git a/actor-sdk/sdk-core/runtime/runtime-android/src/main/java/im/actor/runtime/android/AndroidThreadingProvider.java b/actor-sdk/sdk-core/runtime/runtime-android/src/main/java/im/actor/runtime/android/AndroidThreadingProvider.java index 4178095469..fb6611a9eb 100644 --- a/actor-sdk/sdk-core/runtime/runtime-android/src/main/java/im/actor/runtime/android/AndroidThreadingProvider.java +++ b/actor-sdk/sdk-core/runtime/runtime-android/src/main/java/im/actor/runtime/android/AndroidThreadingProvider.java @@ -10,6 +10,7 @@ import android.content.IntentFilter; import android.content.SharedPreferences; +import im.actor.runtime.util.ExponentialBackoff; import im.actor.runtime.Runtime; import im.actor.runtime.actors.ThreadPriority; import im.actor.runtime.android.threading.AndroidDispatcher; @@ -57,8 +58,14 @@ private void invalidateSync() { @Override public void run() { SntpClient client = new SntpClient(); + ExponentialBackoff exponentialBackoff = new ExponentialBackoff(); while (!client.requestTime(serverHost, 10000)) { - + exponentialBackoff.onFailure(); + try { + Thread.sleep(exponentialBackoff.exponentialWait()); + } catch (InterruptedException e) { + e.printStackTrace(); + } } syncDelta = client.getClockOffset(); preference.edit().putLong("delta", syncDelta).commit(); diff --git a/actor-sdk/sdk-core/runtime/runtime-generic/src/main/java/im/actor/runtime/generic/mvvm/BindedDisplayList.java b/actor-sdk/sdk-core/runtime/runtime-generic/src/main/java/im/actor/runtime/generic/mvvm/BindedDisplayList.java index 4ef8c4666c..c4b14750fd 100644 --- a/actor-sdk/sdk-core/runtime/runtime-generic/src/main/java/im/actor/runtime/generic/mvvm/BindedDisplayList.java +++ b/actor-sdk/sdk-core/runtime/runtime-generic/src/main/java/im/actor/runtime/generic/mvvm/BindedDisplayList.java @@ -233,7 +233,7 @@ public void onLoaded(List items, long topSortKey, long bottomSortKey) { public void initCenter(long centerSortKey, boolean refresh) { // im.actor.runtime.Runtime.checkMainThread(); - if (mode != null && mode == ListMode.CENTER) { + if (!refresh && mode != null && mode == ListMode.CENTER) { return; } mode = ListMode.CENTER; @@ -353,6 +353,9 @@ public void onLoaded(List items, long topSortKey, long bottomSortKey) { if (items.size() == 0) { window.onForwardCompleted(); + if (bindHook != null) { + bindHook.onScrolledToEnd(); + } // Log.d(TAG, "isLoadMoreForwardRequested = false: sync"); isLoadMoreForwardRequested = false; } else { diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/actors/Actor.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/actors/Actor.java index 5ded2ee503..b1b188d244 100755 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/actors/Actor.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/actors/Actor.java @@ -93,7 +93,9 @@ public void unstashAll(int index) { if (stashedMessages == null || stashedMessages.size() == 0) { return; } - for (StashedMessage stashedMessage : stashedMessages) { + StashedMessage stashedMessage; + for (int i = stashedMessages.size() - 1; i >= 0; i--) { + stashedMessage = stashedMessages.get(i); self().sendFirst(stashedMessage.getMessage(), stashedMessage.getSender()); } stashedMessages.clear(); diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/box/ActorBox.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/box/ActorBox.java index f98143f1f2..84b44a2d7e 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/box/ActorBox.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/crypto/box/ActorBox.java @@ -51,7 +51,7 @@ public static byte[] openBox(byte[] header, byte[] cipherText, ActorBoxKey key) } PKCS7Padding padding = new PKCS7Padding(); if (!padding.validate(plainText, plainText.length - 1 - paddingSize, paddingSize)) { - throw new IntegrityException("Padding does not match!"); + throw new IntegrityException("Padding does not isMatch!"); } return ByteStrings.substring(plainText, 0, plainText.length - 1 - paddingSize); diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/markdown/MarkdownParser.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/markdown/MarkdownParser.java index 9d8387453b..dcb0b6eac8 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/markdown/MarkdownParser.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/markdown/MarkdownParser.java @@ -374,10 +374,10 @@ private BasicUrl findUrl(TextCursor cursor, int limit) { * @return is good anchor */ private boolean isGoodAnchor(String text, int index) { - // Check if there is space after block + // Check if there is space and punctuation mark after block + String punct = " .,:!?\t\n"; if (index >= 0 && index < text.length()) { - char postfix = text.charAt(index); - if (postfix != ' ' && postfix != '\t' && postfix != '\n') { + if (punct.indexOf(text.charAt(index)) == -1) { return false; } } diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/markdown/Patterns.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/markdown/Patterns.java index e57e2d80ca..a4f0e3629c 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/markdown/Patterns.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/markdown/Patterns.java @@ -63,7 +63,7 @@ public class Patterns { public static final String WEB_URL_START_CHAR = "^" + WEB_URL_CHAR; /** - * Regular expression pattern to match most part of RFC 3987 + * Regular expression pattern to isMatch most part of RFC 3987 * Internationalized URLs, aka IRIs. Commonly used Unicode characters are * added. */ @@ -130,7 +130,7 @@ public static final String concatGroups(MatcherCompat matcher) { * be extracted * * @return A String comprising all of the digits and plus in - * the match + * the isMatch */ public static final String digitsAndPlusOnly(MatcherCompat matcher) { StringBuilder buffer = new StringBuilder(); diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mtproto/ConnectionEndpoint.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mtproto/ConnectionEndpoint.java index 8e4fd87b66..44e722a901 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mtproto/ConnectionEndpoint.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mtproto/ConnectionEndpoint.java @@ -10,7 +10,14 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public class ConnectionEndpoint { +import java.io.IOException; + +import im.actor.runtime.bser.Bser; +import im.actor.runtime.bser.BserObject; +import im.actor.runtime.bser.BserValues; +import im.actor.runtime.bser.BserWriter; + +public class ConnectionEndpoint extends BserObject { public static final int TYPE_TCP = 0; public static final int TYPE_TCP_TLS = 1; @@ -28,6 +35,9 @@ public class ConnectionEndpoint { @Property("readonly, nonatomic") private int type; + public ConnectionEndpoint() { + } + @ObjectiveCName("initWithHost:withPort:withKnownIp:withType:") public ConnectionEndpoint(@NotNull String host, int port, @Nullable String knownIp, int type) { this.host = host; @@ -53,4 +63,40 @@ public int getPort() { public String getKnownIp() { return knownIp; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ConnectionEndpoint that = (ConnectionEndpoint) o; + + if (port != that.port) return false; + if (type != that.type) return false; + if (!host.equals(that.host)) return false; + return !(knownIp != null ? !knownIp.equals(that.knownIp) : that.knownIp != null); + + } + + public static ConnectionEndpoint fromBytes(byte[] data) throws IOException { + return Bser.parse(new ConnectionEndpoint(), data); + } + + @Override + public void parse(BserValues values) throws IOException { + host = values.getString(1); + knownIp = values.optString(2); + port = values.getInt(3); + type = values.getInt(4); + } + + @Override + public void serialize(BserWriter writer) throws IOException { + writer.writeString(1, host); + if (knownIp != null) { + writer.writeString(2, knownIp); + } + writer.writeInt(3, port); + writer.writeInt(4, type); + } } diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mtproto/ConnectionEndpointArray.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mtproto/ConnectionEndpointArray.java new file mode 100644 index 0000000000..7f99a848c8 --- /dev/null +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mtproto/ConnectionEndpointArray.java @@ -0,0 +1,67 @@ +package im.actor.runtime.mtproto; + +import com.google.j2objc.annotations.ObjectiveCName; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; + +public class ConnectionEndpointArray extends ArrayList { + @NotNull + @ObjectiveCName("addEndpoint:") + public ConnectionEndpointArray addEndpoint(@NotNull String url) throws UnknownSchemeException { + + // Manual buggy parsing for GWT + // TODO: Correct URL parsing + String scheme = url.substring(0, url.indexOf(":")).toLowerCase(); + String host = url.substring(url.indexOf("://") + "://".length()); + String knownIp = null; + + if (host.endsWith("/")) { + host = host.substring(0, host.length() - 1); + } + int port = -1; + if (host.contains(":")) { + String[] parts = host.split(":"); + host = parts[0]; + port = Integer.parseInt(parts[1]); + } + + if (host.contains("@")) { + String[] parts = host.split("@"); + host = parts[0]; + knownIp = parts[1]; + } + + if (scheme.equals("ssl") || scheme.equals("tls")) { + if (port <= 0) { + port = 443; + } + add(new ConnectionEndpoint(host, port, knownIp, ConnectionEndpoint.TYPE_TCP_TLS)); + } else if (scheme.equals("tcp")) { + if (port <= 0) { + port = 80; + } + add(new ConnectionEndpoint(host, port, knownIp, ConnectionEndpoint.TYPE_TCP)); + } else if (scheme.equals("ws")) { + if (port <= 0) { + port = 80; + } + add(new ConnectionEndpoint(host, port, knownIp, ConnectionEndpoint.TYPE_WS)); + } else if (scheme.equals("wss")) { + if (port <= 0) { + port = 443; + } + add(new ConnectionEndpoint(host, port, knownIp, ConnectionEndpoint.TYPE_WS_TLS)); + } else { + throw new UnknownSchemeException("Unknown scheme type: " + scheme); + } + return this; + } + + public class UnknownSchemeException extends Exception { + public UnknownSchemeException(String message) { + super(message); + } + } +} \ No newline at end of file diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/AsyncVM.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/AsyncVM.java index 384f58565d..8c6eb0122e 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/AsyncVM.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/AsyncVM.java @@ -9,12 +9,9 @@ public abstract class AsyncVM { private boolean isDetached; protected final void post(final Object obj) { - im.actor.runtime.Runtime.postToMainThread(new Runnable() { - @Override - public void run() { - if (!isDetached) { - onObjectReceived(obj); - } + im.actor.runtime.Runtime.postToMainThread(() -> { + if (!isDetached) { + onObjectReceived(obj); } }); } diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/SearchValueModel.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/SearchValueModel.java new file mode 100644 index 0000000000..ae74a86a35 --- /dev/null +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/SearchValueModel.java @@ -0,0 +1,88 @@ +package im.actor.runtime.mvvm; + +import java.util.ArrayList; +import java.util.List; + +import im.actor.runtime.annotations.MainThread; + +public class SearchValueModel extends AsyncVM { + + private SearchValueSource searchValueSource; + private int requestId = 0; + private ValueModel> results; + + public SearchValueModel(SearchValueSource searchValueSource) { + this.searchValueSource = searchValueSource; + this.results = new ValueModel<>("search.results", new ArrayList<>()); + } + + public ValueModel> getResults() { + return results; + } + + @MainThread + public void queryChanged(String query) { + + final int currentRequestId = ++requestId; + + // Filtering out trivial sources + if (query == null) { + postResults(new ArrayList<>(), currentRequestId); + return; + } + query = query.trim(); + if (query.length() == 0) { + postResults(new ArrayList<>(), currentRequestId); + return; + } + + // Non-trivial + searchValueSource.loadResults(query, r -> { + if (currentRequestId == requestId) { + postResults(r, currentRequestId); + } + }); + } + + @MainThread + protected void onResultsReceived(List res) { + results.changeInUIThread(res); + } + + // + // Internal Loop + // + + private void postResults(List res, int requestIndex) { + post(new Results<>(res, requestIndex)); + } + + @Override + protected void onObjectReceived(Object obj) { + if (obj instanceof Results) { + Results r = (Results) obj; + if (r.getRequestIndex() == requestId) { + onResultsReceived(r.getRes()); + } + } + } + + protected static class Results { + + private List res; + private int requestIndex; + + public Results(List res, int requestIndex) { + this.res = res; + this.requestIndex = requestIndex; + } + + public List getRes() { + return res; + } + + public int getRequestIndex() { + return requestIndex; + } + } +} \ No newline at end of file diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/SearchValueSource.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/SearchValueSource.java new file mode 100644 index 0000000000..c20a78b864 --- /dev/null +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/SearchValueSource.java @@ -0,0 +1,9 @@ +package im.actor.runtime.mvvm; + +import java.util.List; + +import im.actor.runtime.function.Consumer; + +public interface SearchValueSource { + void loadResults(String query, Consumer> callback); +} diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/Value.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/Value.java index ad5a6af1b5..07e26acdf5 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/Value.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/Value.java @@ -11,7 +11,7 @@ */ public abstract class Value { - private ArrayList> listeners = new ArrayList>(); + private ArrayList> listeners = new ArrayList<>(); private String name; @@ -99,8 +99,7 @@ protected void notify(final T value) { * @param value new value */ protected void notifyInMainThread(final T value) { - for (ValueChangedListener listener : - listeners.toArray(new ValueChangedListener[listeners.size()])) { + for (ValueChangedListener listener : listeners) { listener.onChanged(value, Value.this); } } diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/ValueDoubleListener.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/ValueDoubleListener.java new file mode 100644 index 0000000000..72c15e4098 --- /dev/null +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/ValueDoubleListener.java @@ -0,0 +1,9 @@ +package im.actor.runtime.mvvm; + +import com.google.j2objc.annotations.ObjectiveCName; + +public interface ValueDoubleListener { + + @ObjectiveCName("onChangedWithVal1:withVal2:") + void onChanged(T1 val1, T2 val2); +} diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/ValueListener.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/ValueListener.java new file mode 100644 index 0000000000..4a9fc5f5da --- /dev/null +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/ValueListener.java @@ -0,0 +1,13 @@ +/* + * Copyright (C) 2015 Actor LLC. + */ + +package im.actor.runtime.mvvm; + +import com.google.j2objc.annotations.ObjectiveCName; + +public interface ValueListener { + + @ObjectiveCName("onChanged:") + void onChanged(T val); +} \ No newline at end of file diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/ValueModel.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/ValueModel.java index 729c0e64a9..d18980bb1c 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/ValueModel.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/mvvm/ValueModel.java @@ -3,6 +3,8 @@ import com.google.j2objc.annotations.ObjectiveCName; import com.google.j2objc.annotations.Property; +import org.jetbrains.annotations.Nullable; + public class ValueModel extends Value { @Property("nonatomic, readonly") @@ -26,7 +28,7 @@ public T get() { * @return is value changed */ @ObjectiveCName("changeWithValue:") - public boolean change(T value) { + public boolean change(@Nullable T value) { if (this.value != null && value != null && value.equals(this.value)) { return false; } @@ -40,7 +42,7 @@ public boolean change(T value) { } @ObjectiveCName("changeNoNotificationWithValue:") - public boolean changeNoNotification(T value) { + public boolean changeNoNotification(@Nullable T value) { if (this.value != null && value != null && value.equals(this.value)) { return false; } @@ -52,7 +54,7 @@ public boolean changeNoNotification(T value) { } @ObjectiveCName("changeInUIThreadWithValue:") - protected boolean changeInUIThread(T value) { + protected boolean changeInUIThread(@Nullable T value) { if (this.value != null && value != null && value.equals(this.value)) { return false; } diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/power/WakeLock.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/power/WakeLock.java index bf29d92cd8..ffcb0b1dc8 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/power/WakeLock.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/power/WakeLock.java @@ -4,6 +4,7 @@ public interface WakeLock { - @ObjectiveCName("releaseLock") + // Don't use "release" prefix as it is conflicts with ObjC runtime + @ObjectiveCName("closeLock") void releaseLock(); } diff --git a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/promise/Promise.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/promise/Promise.java index 260cd1e5b1..88cf371185 100644 --- a/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/promise/Promise.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/promise/Promise.java @@ -223,31 +223,34 @@ synchronized void tryResult(@Nullable T res) { /** * Delivering result */ - private void deliverResult() { + protected void deliverResult() { if (callbacks.size() > 0) { dispatcher.dispatch(() -> { - if (exception != null) { - for (PromiseCallback callback : callbacks) { - try { - callback.onError(exception); - } catch (Exception e) { - e.printStackTrace(); - } - } - } else { - for (PromiseCallback callback : callbacks) { - try { - callback.onResult(result); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - callbacks.clear(); + invokeDeliver(); }); } } - + + protected void invokeDeliver() { + if (exception != null) { + for (PromiseCallback callback : callbacks) { + try { + callback.onError(exception); + } catch (Exception e) { + e.printStackTrace(); + } + } + } else { + for (PromiseCallback callback : callbacks) { + try { + callback.onResult(result); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + callbacks.clear(); + } // // Conversions diff --git a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/util/ExponentialBackoff.java b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/util/ExponentialBackoff.java similarity index 98% rename from actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/util/ExponentialBackoff.java rename to actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/util/ExponentialBackoff.java index cd1a6f8349..43aabdcd75 100644 --- a/actor-sdk/sdk-core/core/core-shared/src/main/java/im/actor/core/util/ExponentialBackoff.java +++ b/actor-sdk/sdk-core/runtime/runtime-shared/src/main/java/im/actor/runtime/util/ExponentialBackoff.java @@ -2,7 +2,7 @@ * Copyright (C) 2015 Actor LLC. */ -package im.actor.core.util; +package im.actor.runtime.util; import java.util.Random; diff --git a/actor-server/Dockerfile b/actor-server/Dockerfile new file mode 100644 index 0000000000..961a13b516 --- /dev/null +++ b/actor-server/Dockerfile @@ -0,0 +1,8 @@ +FROM openjdk:8u92-jre-alpine +MAINTAINER Actor LLC +RUN apk --update add bash openssl apr +ADD target/docker/stage/var /var +ADD templates /var/lib/actor/templates +ENTRYPOINT bin/actor +WORKDIR /var/lib/actor +EXPOSE 9070 9080 9090 diff --git a/actor-server/actor-activation/src/main/scala/im/actor/server/activation/smtp/SMTPProvider.scala b/actor-server/actor-activation/src/main/scala/im/actor/server/activation/smtp/SMTPProvider.scala index 459d9d92f7..bdea067324 100644 --- a/actor-server/actor-activation/src/main/scala/im/actor/server/activation/smtp/SMTPProvider.scala +++ b/actor-server/actor-activation/src/main/scala/im/actor/server/activation/smtp/SMTPProvider.scala @@ -12,13 +12,14 @@ import im.actor.env.ActorEnv import im.actor.server.activation.common.ActivationStateActor.{ ForgetSentCode, Send, SendAck } import im.actor.server.activation.common._ import im.actor.server.db.DbExtension -import im.actor.server.email.{ Content, EmailConfig, Message, SmtpEmailSender } +import im.actor.server.email._ import im.actor.server.model.AuthEmailTransaction import im.actor.server.persist.auth.AuthTransactionRepo import im.actor.util.misc.EmailUtils.isTestEmail import scala.concurrent.Future import scala.concurrent.duration._ +import scala.util.Try private[activation] final class SMTPProvider(system: ActorSystem) extends ActivationProvider with CommonAuthCodes { @@ -28,12 +29,14 @@ private[activation] final class SMTPProvider(system: ActorSystem) extends Activa private implicit val timeout = Timeout(20.seconds) - private val emailConfig = EmailConfig.load.getOrElse(throw new RuntimeException("Failed to load email config")) - private val emailSender = new SmtpEmailSender(emailConfig) + private val emailSender = EmailExtension(system).sender + private val emailTemplateLocation = ActorEnv.getAbsolutePath(Paths.get(ActorConfig.load().getString("services.activation.email.template"))) - private val emailTemplate = new String(Files.readAllBytes(emailTemplateLocation)) + private val emailTemplate = + Try(new String(Files.readAllBytes(emailTemplateLocation))) + .getOrElse(throw new RuntimeException(s"Failed to read template file. Make sure you put it in ${emailTemplateLocation}")) private val smtpStateActor = system.actorOf(ActivationStateActor.props[String, EmailCode]( repeatLimit = activationConfig.repeatLimit, diff --git a/actor-server/actor-bots-shared/src/main/scala/im/actor/bots/BotMessages.scala b/actor-server/actor-bots-shared/src/main/scala/im/actor/bots/BotMessages.scala index be715cd660..294e3a7d95 100644 --- a/actor-server/actor-bots-shared/src/main/scala/im/actor/bots/BotMessages.scala +++ b/actor-server/actor-bots-shared/src/main/scala/im/actor/bots/BotMessages.scala @@ -542,7 +542,7 @@ object BotMessages { @key("CreateGroup") final case class CreateGroup( - title: String + @beanGetter title: String ) extends RequestBody { override type Response = ResponseCreateGroup override val service: String = Services.Groups @@ -550,8 +550,75 @@ object BotMessages { override def readResponse(obj: Js.Obj): Response = readJs[Response](obj) } + @key("CreateGroupWithOwner") + final case class CreateGroupWithOwner( + @beanGetter title: String, + @beanGetter user: UserPeer, + members: Seq[UserPeer] + ) extends RequestBody { + override type Response = ResponseCreateGroup + override val service: String = Services.Groups + + def this(title: String, user: UserPeer, members: java.util.List[UserPeer]) = + this(title, user, members.toIndexedSeq) + + def getMembers = seqAsJavaList(members) + + override def readResponse(obj: Js.Obj): Response = readJs[Response](obj) + } + final case class ResponseCreateGroup(@beanGetter peer: GroupOutPeer) extends ResponseBody + @key("UpdateGroupShortName") + final case class UpdateGroupShortName( + @beanGetter groupId: Int, + shortName: Option[String] + ) extends RequestBody { + def this(groupId: Int, shortName: String) = this(groupId, Option(shortName)) + + def getShortName = shortName.asJava + + override type Response = Void + override val service: String = Services.Groups + + override def readResponse(obj: Js.Obj): Response = readJs[Response](obj) + } + + @key("AddGroupExtString") + final case class AddGroupExtString( + @beanGetter groupId: Int, + @beanGetter key: String, + @beanGetter value: String + ) extends RequestBody { + override type Response = Void + override val service: String = Services.Groups + + override def readResponse(obj: Js.Obj): Response = readJs[Response](obj) + } + + @key("AddGroupExtBool") + final case class AddGroupExtBool( + @beanGetter groupId: Int, + @beanGetter key: String, + @beanGetter value: Boolean + ) extends RequestBody { + override type Response = Void + override val service: String = Services.Groups + + override def readResponse(obj: Js.Obj): Response = readJs[Response](obj) + } + + @key("RemoveGroupExt") + final case class RemoveGroupExt( + @beanGetter groupId: Int, + @beanGetter key: String + ) extends RequestBody { + override type Response = Void + override val service: String = Services.Groups + + override def readResponse(obj: Js.Obj): Response = readJs[Response](obj) + } + @key("InviteUser") final case class InviteUser(@beanGetter groupPeer: GroupOutPeer, @beanGetter userPeer: UserOutPeer) extends RequestBody { override type Response = Void @@ -849,8 +916,8 @@ object BotMessages { @key("AnimationVid") final case class DocumentExAnimationVid( - @beanGetter width: Int, - @beanGetter height: Int, + @beanGetter width: Int, + @beanGetter height: Int, @beanGetter duration: Int ) extends DocumentEx diff --git a/actor-server/actor-bots/src/main/scala/im/actor/server/bot/http/BotsHttpHandler.scala b/actor-server/actor-bots/src/main/scala/im/actor/server/bot/http/BotsHttpHandler.scala index 8280d58e55..28be1f9590 100644 --- a/actor-server/actor-bots/src/main/scala/im/actor/server/bot/http/BotsHttpHandler.scala +++ b/actor-server/actor-bots/src/main/scala/im/actor/server/bot/http/BotsHttpHandler.scala @@ -12,7 +12,7 @@ import akka.stream.{ ActorMaterializer, Materializer } import akka.stream.scaladsl.Flow import akka.util.ByteString import cats.data.OptionT -import cats.std.future._ +import cats.instances.future._ import de.heikoseeberger.akkahttpplayjson.PlayJsonSupport import im.actor.api.rpc.sequence.UpdateRawUpdate import im.actor.server.api.http.HttpHandler diff --git a/actor-server/actor-bots/src/main/scala/im/actor/server/bot/services/GroupsBotService.scala b/actor-server/actor-bots/src/main/scala/im/actor/server/bot/services/GroupsBotService.scala index 494f6fb9fe..a96984ddc6 100644 --- a/actor-server/actor-bots/src/main/scala/im/actor/server/bot/services/GroupsBotService.scala +++ b/actor-server/actor-bots/src/main/scala/im/actor/server/bot/services/GroupsBotService.scala @@ -1,12 +1,20 @@ package im.actor.server.bot.services import akka.actor.ActorSystem +import im.actor.bots.BotMessages.BotError import im.actor.server.bot.{ ApiToBotConversions, BotServiceBase } -import im.actor.server.group.{ GroupExtension, GroupType } +import im.actor.server.group.GroupErrors.{ InvalidExtension, InvalidShortName, NoPermission, ShortNameTaken } +import im.actor.server.group.GroupExt.Value.{ BoolValue, StringValue } +import im.actor.server.group.{ GroupExt, GroupExtension } import im.actor.util.misc.IdUtils +import scala.concurrent.Future import scala.concurrent.forkjoin.ThreadLocalRandom +private[bot] object GroupsBotErrors { + val Forbidden = BotError(403, "FORBIDDEN") +} + private[bot] final class GroupsBotService(system: ActorSystem) extends BotServiceBase(system) with ApiToBotConversions { import im.actor.bots.BotMessages._ @@ -15,25 +23,62 @@ private[bot] final class GroupsBotService(system: ActorSystem) extends BotServic private val groupExt = GroupExtension(system) override val handlers: Handlers = { - case CreateGroup(title) ⇒ createGroup(title).toWeak - case InviteUser(groupPeer, userPeer) ⇒ inviteUser(groupPeer, userPeer).toWeak + case CreateGroup(title) ⇒ createGroup(title).toWeak + case CreateGroupWithOwner(title, owner, members) ⇒ createGroupWithOwner(title, owner, members).toWeak + case UpdateGroupShortName(groupId, shortName) ⇒ updateShortName(groupId, shortName).toWeak + case AddGroupExtString(groupId, key, value) ⇒ addGroupExtString(groupId, key, value).toWeak + case AddGroupExtBool(groupId, key, value) ⇒ addGroupExtBool(groupId, key, value).toWeak + case RemoveGroupExt(groupId, key) ⇒ removeExt(groupId, key).toWeak + case InviteUser(groupPeer, userPeer) ⇒ inviteUser(groupPeer, userPeer).toWeak } private def createGroup(title: String) = RequestHandler[CreateGroup, CreateGroup#Response]( - (botUserId: Int, botAuthId: Long, botAuthSid: Int) ⇒ { - val groupId = IdUtils.nextIntId() - val randomId = ThreadLocalRandom.current().nextLong() + (botUserId: Int, _: Long, _: Int) ⇒ { + create(title, botUserId, Set.empty) + } + ) - for { - ack ← groupExt.create( - groupId = groupId, - clientUserId = botUserId, - clientAuthId = 0L, - title = title, - randomId = randomId, - userIds = Set.empty - ) - } yield Right(ResponseCreateGroup(GroupOutPeer(groupId, ack.accessHash))) + private def createGroupWithOwner(title: String, owner: UserPeer, members: Seq[UserPeer]) = RequestHandler[CreateGroup, CreateGroup#Response]( + (botUserId: Int, _: Long, _: Int) ⇒ { + ifIsAdmin(botUserId) { + create(title, owner.id, (members map (_.id)).toSet) + } + } + ) + + private def create(title: String, ownerUserId: Int, memberIds: Set[Int]) = { + val groupId = IdUtils.nextIntId() + val randomId = ThreadLocalRandom.current().nextLong() + + for { + ack ← groupExt.create( + groupId = groupId, + clientUserId = ownerUserId, + clientAuthId = 0L, + title = title, + randomId = randomId, + userIds = memberIds + ) + } yield Right(ResponseCreateGroup(GroupOutPeer(groupId, ack.accessHash))) + } + + private def updateShortName(groupId: Int, shortName: Option[String]) = RequestHandler[UpdateGroupShortName, UpdateGroupShortName#Response]( + (botUserId: Int, _: Long, _: Int) ⇒ { + ifIsAdmin(botUserId) { + (for { + group ← groupExt.getApiFullStruct(groupId, 0) + _ ← groupExt.updateShortName( + groupId = groupId, + clientUserId = group.ownerUserId.getOrElse(0), + clientAuthId = 0L, + shortName = shortName + ) + } yield Right(Void)) recover { + case NoPermission ⇒ Left(GroupsBotErrors.Forbidden) + case InvalidShortName ⇒ Left(BotError(400, "INVALID_SHORT_NAME")) + case ShortNameTaken ⇒ Left(BotError(400, "SHORT_NAME_ALREADY_TAKEN")) + } + } } ) @@ -48,4 +93,35 @@ private[bot] final class GroupsBotService(system: ActorSystem) extends BotServic } yield Right(Void) } ) + + private def addGroupExtString(groupId: Int, key: String, value: String) = RequestHandler[AddGroupExtString, AddGroupExtString#Response]( + (botUserId: Int, _: Long, _: Int) ⇒ { + ifIsAdmin(botUserId) { + (for (_ ← addExt(groupId, GroupExt(key, StringValue(value)))) yield Right(Void)) recover { + case InvalidExtension ⇒ Left(BotError(500, "INVALID_EXT")) + } + } + } + ) + + private def addGroupExtBool(groupId: Int, key: String, value: Boolean) = RequestHandler[AddGroupExtBool, AddGroupExtBool#Response]( + (botUserId: Int, botAuthId: Long, botAuthSid: Int) ⇒ { + ifIsAdmin(botUserId) { + (for (_ ← addExt(groupId, GroupExt(key, BoolValue(value)))) yield Right(Void)) recover { + case InvalidExtension ⇒ Left(BotError(500, "INVALID_EXT")) + } + } + } + ) + + private def removeExt(groupId: Int, key: String) = RequestHandler[RemoveGroupExt, RemoveGroupExt#Response]( + (botUserId: Int, botAuthId: Long, botAuthSid: Int) ⇒ { + ifIsAdmin(botUserId) { + groupExt.removeExt(groupId, key) map (_ ⇒ Right(Void)) + } + } + ) + + private def addExt(groupId: Int, ext: GroupExt): Future[Unit] = groupExt.addExt(groupId: Int, ext: GroupExt) + } diff --git a/actor-server/actor-cli/src/main/scala/im/actor/server/cli/ActorCli.scala b/actor-server/actor-cli/src/main/scala/im/actor/server/cli/ActorCli.scala index ca1a3538d4..65b99fabb9 100644 --- a/actor-server/actor-cli/src/main/scala/im/actor/server/cli/ActorCli.scala +++ b/actor-server/actor-cli/src/main/scala/im/actor/server/cli/ActorCli.scala @@ -15,11 +15,12 @@ import scala.concurrent.duration._ import scala.reflect.ClassTag private case class Config( - command: String = "help", - createBot: CreateBot = CreateBot(), - updateIsAdmin: UpdateIsAdmin = UpdateIsAdmin(), - httpToken: HttpToken = HttpToken(), - key: Key = Key() + command: String = "help", + createBot: CreateBot = CreateBot(), + updateIsAdmin: UpdateIsAdmin = UpdateIsAdmin(), + httpToken: HttpToken = HttpToken(), + key: Key = Key(), + host: Option[String] = None // remote actor system host ) private[cli] trait Request { @@ -75,6 +76,9 @@ object ActorCli extends App { cmd(Commands.CreateBot) action { (_, c) ⇒ c.copy(command = Commands.CreateBot) } children ( + opt[String]("host") abbr "h" optional () action { (x, c) ⇒ + c.copy(host = Some(x)) + }, opt[String]("username") abbr "u" required () action { (x, c) ⇒ c.copy(createBot = c.createBot.copy(username = x)) }, @@ -88,6 +92,9 @@ object ActorCli extends App { cmd(Commands.AdminGrant) action { (_, c) ⇒ c.copy(command = Commands.AdminGrant) } children ( + opt[String]("host") abbr "h" optional () action { (x, c) ⇒ + c.copy(host = Some(x)) + }, opt[Int]("userId") abbr "u" required () action { (x, c) ⇒ c.copy(updateIsAdmin = UpdateIsAdmin(x, isAdmin = true)) } @@ -95,19 +102,29 @@ object ActorCli extends App { cmd(Commands.AdminRevoke) action { (_, c) ⇒ c.copy(command = Commands.AdminRevoke) } children ( + opt[String]("host") abbr "h" optional () action { (x, c) ⇒ + c.copy(host = Some(x)) + }, opt[Int]("userId") abbr "u" required () action { (x, c) ⇒ c.copy(updateIsAdmin = UpdateIsAdmin(x, isAdmin = false)) } ) cmd(Commands.MigrateUserSequence) action { (_, c) ⇒ c.copy(command = Commands.MigrateUserSequence) - } + } children ( + opt[String]("host") abbr "h" optional () action { (x, c) ⇒ + c.copy(host = Some(x)) + } + ) cmd(Commands.HttpToken) action { (_, c) ⇒ c.copy(command = Commands.HttpToken) } children ( cmd("create") action { (_, c) ⇒ c.copy(httpToken = c.httpToken.copy(command = "create")) } children ( + opt[String]("host") abbr "h" optional () action { (x, c) ⇒ + c.copy(host = Some(x)) + }, opt[Unit]("admin") abbr "a" optional () action { (x, c) ⇒ c.copy(httpToken = c.httpToken.copy(create = c.httpToken.create.copy(isAdmin = true))) } @@ -116,6 +133,9 @@ object ActorCli extends App { cmd(Commands.Key) action { (_, c) ⇒ c.copy(command = Commands.Key) } children ( + opt[String]("host") abbr "h" optional () action { (x, c) ⇒ + c.copy(host = Some(x)) + }, opt[Unit]("create") abbr "c" required () action { (x, c) ⇒ c.copy(key = c.key.copy(create = true)) }, @@ -126,7 +146,7 @@ object ActorCli extends App { } parser.parse(args, Config()) foreach { config ⇒ - val handlers = new CliHandlers + val handlers = new CliHandlers(config.host) val migrationHandlers = new MigrationHandlers val securityHandlers = new SecurityHandlers @@ -157,7 +177,7 @@ object ActorCli extends App { } } -final class CliHandlers extends BotHandlers with UsersHandlers with HttpHandlers { +final class CliHandlers(host: Option[String]) extends BotHandlers with UsersHandlers with HttpHandlers { protected val BotService = "bots" protected val UsersService = "users" protected val HttpService = "http" @@ -169,7 +189,7 @@ final class CliHandlers extends BotHandlers with UsersHandlers with HttpHandlers ActorSystem("actor-cli", config) } - protected lazy val remoteHost = InetAddress.getLocalHost.getHostAddress + protected lazy val remoteHost = host.getOrElse(InetAddress.getLocalHost.getHostAddress) protected lazy val initialContacts = Set(ActorPath.fromString(s"akka.tcp://actor-server@$remoteHost:2552/system/receptionist")) diff --git a/actor-server/actor-core/src/main/actor-api/actor.json b/actor-server/actor-core/src/main/actor-api/actor.json index ef48ff2e50..544ad17482 100644 --- a/actor-server/actor-core/src/main/actor-api/actor.json +++ b/actor-server/actor-core/src/main/actor-api/actor.json @@ -6601,6 +6601,32 @@ ] } }, + { + "type": "update", + "content": { + "name": "ChatDropCache", + "header": 2690, + "doc": [ + "Update about cache drop", + { + "type": "reference", + "argument": "peer", + "category": "full", + "description": " Destination peer" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "Peer" + }, + "id": 1, + "name": "peer" + } + ] + } + }, { "type": "update", "content": { @@ -6611,7 +6637,7 @@ { "type": "reference", "argument": "dialogs", - "category": "full", + "category": "compact", "description": " New dialgos list" } ], @@ -7876,12 +7902,116 @@ ] } }, + { + "type": "enum", + "content": { + "name": "GroupPermissions", + "values": [ + { + "name": "SEND_MESSAGE", + "id": 1 + }, + { + "name": "CLEAR", + "id": 2 + }, + { + "name": "LEAVE", + "id": 3 + }, + { + "name": "DELETE", + "id": 4 + }, + { + "name": "JOIN", + "id": 5 + }, + { + "name": "VIEW_INFO", + "id": 6 + } + ] + } + }, + { + "type": "enum", + "content": { + "name": "GroupFullPermissions", + "values": [ + { + "name": "EDIT_INFO", + "id": 1 + }, + { + "name": "VIEW_MEMBERS", + "id": 2 + }, + { + "name": "INVITE_MEMBERS", + "id": 3 + }, + { + "name": "INVITE_VIA_LINK", + "id": 4 + }, + { + "name": "CALL", + "id": 5 + }, + { + "name": "EDIT_ADMIN_SETTINGS", + "id": 6 + }, + { + "name": "VIEW_ADMINS", + "id": 7 + }, + { + "name": "EDIT_ADMINS", + "id": 8 + }, + { + "name": "KICK_INVITED", + "id": 9 + }, + { + "name": "KICK_ANYONE", + "id": 10 + }, + { + "name": "EDIT_FOREIGN", + "id": 11 + }, + { + "name": "DELETE_FOREIGN", + "id": 12 + } + ] + } + }, { "type": "struct", "content": { "name": "Group", "doc": [ "Group information", + "", + "Permissions.", + "Permissions of this structure is about group messages operation, such as", + "ability to send messages, clear chat, leave group and so on. This operations", + "Can be held outside of the Group Info page.", + "", + "Default value is ZERO, Opposide iz ONE. If Default is FALSE then ONE == TRUE.", + "If default is TRUE then ONE == FALSE.", + "Bits:", + "0 - canSendMessage. Default is FALSE.", + "1 - canClear. Default is FALSE.", + "2 - canLeave. Default is FALSE.", + "3 - canDelete. Default is FALSE.", + "4 - canJoin. Default is FALSE.", + "5 - canViewInfo. Default is FALSE.", + "", { "type": "reference", "argument": "id", @@ -7932,9 +8062,15 @@ }, { "type": "reference", - "argument": "canSendMessage", + "argument": "permissions", + "category": "full", + "description": " Permissions of group object" + }, + { + "type": "reference", + "argument": "isDeleted", "category": "full", - "description": " Can user send messages. Default is equals isMember for Group and false for others." + "description": " Is this group deleted" }, { "type": "reference", @@ -8048,10 +8184,18 @@ { "type": { "type": "opt", - "childType": "bool" + "childType": "int64" }, "id": 26, - "name": "canSendMessage" + "name": "permissions" + }, + { + "type": { + "type": "opt", + "childType": "bool" + }, + "id": 27, + "name": "isDeleted" }, { "type": { @@ -8070,7 +8214,8 @@ "childType": "bool" }, "id": 16, - "name": "isAdmin" + "name": "isAdmin", + "deprecated": "true" }, { "type": { @@ -8078,7 +8223,8 @@ "childType": "userId" }, "id": 8, - "name": "creatorUid" + "name": "creatorUid", + "deprecated": "true" }, { "type": { @@ -8089,7 +8235,8 @@ } }, "id": 9, - "name": "members" + "name": "members", + "deprecated": "true" }, { "type": { @@ -8097,7 +8244,8 @@ "childType": "date" }, "id": 10, - "name": "createDate" + "name": "createDate", + "deprecated": "true" }, { "type": { @@ -8105,7 +8253,8 @@ "childType": "string" }, "id": 17, - "name": "theme" + "name": "theme", + "deprecated": "true" }, { "type": { @@ -8113,7 +8262,8 @@ "childType": "string" }, "id": 18, - "name": "about" + "name": "about", + "deprecated": "true" } ] } @@ -8124,6 +8274,26 @@ "name": "GroupFull", "doc": [ "Goup Full information", + "Permissions.", + "Idea of Group Full mermissions is about Group Info pages. This permissions", + "are usefull only when trying to view and update group settings and not related", + "to chat messages itself.", + "Default value is ZERO, Opposide iz ONE. If Default is FALSE then ONE == TRUE.", + "If default is TRUE then ONE == FALSE.", + "Bits:", + "0 - canEditInfo. Default is FALSE.", + "1 - canViewMembers. Default is FALSE.", + "2 - canInviteMembers. Default is FALSE.", + "3 - canInviteViaLink. Default is FALSE.", + "4 - canCall. Default is FALSE.", + "5 - canEditAdminSettings. Default is FALSE.", + "6 - canViewAdmins. Default is FALSE.", + "7 - canEditAdmins. Default is FALSE.", + "8 - canKickInvited. Default is FALSE.", + "9 - canKickAnyone. Default is FALSE.", + "10 - canEditForeign. Default is FALSE.", + "11 - canDeleteForeign. Default is FALSE.", + "", { "type": "reference", "argument": "id", @@ -8140,7 +8310,7 @@ "type": "reference", "argument": "ownerUid", "category": "full", - "description": " Group owner" + "description": " Optional group owner" }, { "type": "reference", @@ -8168,23 +8338,24 @@ }, { "type": "reference", - "argument": "canViewMembers", + "argument": "isSharedHistory", "category": "full", - "description": " Can current user view members of the group. Default is true." + "description": " Is history shared among all users. Default is false." }, { "type": "reference", - "argument": "canInvitePeople", + "argument": "shortName", "category": "full", - "description": " Can current user invite new people. Default is true." + "description": " Group's short name" }, { "type": "reference", - "argument": "isSharedHistory", + "argument": "permissions", "category": "full", - "description": " Is history shared among all users. Default is false." + "description": " Group Permissions" } ], + "expandable": "true", "attributes": [ { "type": { @@ -8204,8 +8375,11 @@ }, { "type": { - "type": "alias", - "childType": "userId" + "type": "opt", + "childType": { + "type": "alias", + "childType": "userId" + } }, "id": 5, "name": "ownerUid" @@ -8261,24 +8435,24 @@ "type": "opt", "childType": "bool" }, - "id": 8, - "name": "canViewMembers" + "id": 10, + "name": "isSharedHistory" }, { "type": { "type": "opt", - "childType": "bool" + "childType": "string" }, - "id": 9, - "name": "canInvitePeople" + "id": 14, + "name": "shortName" }, { "type": { "type": "opt", - "childType": "bool" + "childType": "int64" }, - "id": 10, - "name": "isSharedHistory" + "id": 27, + "name": "permissions" } ] } @@ -8348,7 +8522,7 @@ "doc": [ { "type": "reference", - "argument": "members", + "argument": "users", "category": "full", "description": " Group members" }, @@ -8360,6 +8534,17 @@ } ], "attributes": [ + { + "type": { + "type": "list", + "childType": { + "type": "struct", + "childType": "Member" + } + }, + "id": 3, + "name": "members" + }, { "type": { "type": "list", @@ -8369,7 +8554,7 @@ } }, "id": 1, - "name": "members" + "name": "users" }, { "type": { @@ -8677,6 +8862,46 @@ ] } }, + { + "type": "update", + "content": { + "name": "GroupShortNameChanged", + "header": 2628, + "doc": [ + "Group's short name changed", + { + "type": "reference", + "argument": "groupId", + "category": "full", + "description": " Group Id" + }, + { + "type": "reference", + "argument": "shortName", + "category": "full", + "description": " Group short name" + } + ], + "attributes": [ + { + "type": { + "type": "alias", + "childType": "groupId" + }, + "id": 1, + "name": "groupId" + }, + { + "type": { + "type": "opt", + "childType": "string" + }, + "id": 2, + "name": "shortName" + } + ] + } + }, { "type": "update", "content": { @@ -8746,21 +8971,15 @@ { "type": "update", "content": { - "name": "GroupCanSendMessagesChanged", - "header": 2624, + "name": "GroupDeleted", + "header": 2658, "doc": [ - "Update about can send messages changed", + "Update about group deleted", { "type": "reference", "argument": "groupId", "category": "full", "description": " Group Id" - }, - { - "type": "reference", - "argument": "canSendMessages", - "category": "full", - "description": " Can send messages" } ], "attributes": [ @@ -8771,11 +8990,6 @@ }, "id": 1, "name": "groupId" - }, - { - "type": "bool", - "id": 2, - "name": "canSendMessages" } ] } @@ -8783,10 +8997,10 @@ { "type": "update", "content": { - "name": "GroupCanViewMembersChanged", - "header": 2625, + "name": "GroupPermissionsChanged", + "header": 2663, "doc": [ - "Update about can view members changed", + "Update about group permissions changed", { "type": "reference", "argument": "groupId", @@ -8795,9 +9009,9 @@ }, { "type": "reference", - "argument": "canViewMembers", + "argument": "permissions", "category": "full", - "description": " Can view members" + "description": " New Permissions" } ], "attributes": [ @@ -8810,9 +9024,9 @@ "name": "groupId" }, { - "type": "bool", + "type": "int64", "id": 2, - "name": "canViewMembers" + "name": "permissions" } ] } @@ -8820,10 +9034,10 @@ { "type": "update", "content": { - "name": "GroupCanInviteMembersChanged", - "header": 2626, + "name": "GroupFullPermissionsChanged", + "header": 2664, "doc": [ - "Update about can invite members changed", + "Update about Full Group permissions changed", { "type": "reference", "argument": "groupId", @@ -8832,9 +9046,9 @@ }, { "type": "reference", - "argument": "canInviteMembers", + "argument": "permissions", "category": "full", - "description": " Can invite members" + "description": " New Permissions" } ], "attributes": [ @@ -8847,9 +9061,9 @@ "name": "groupId" }, { - "type": "bool", + "type": "int64", "id": 2, - "name": "canInviteMembers" + "name": "permissions" } ] } @@ -9571,6 +9785,50 @@ ] } }, + { + "type": "rpc", + "content": { + "name": "EditGroupShortName", + "header": 2793, + "response": { + "type": "reference", + "name": "Seq" + }, + "doc": [ + "Edit Group Short Name", + { + "type": "reference", + "argument": "groupPeer", + "category": "full", + "description": "Group's peer" + }, + { + "type": "reference", + "argument": "shortName", + "category": "full", + "description": "New group's short name" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "GroupOutPeer" + }, + "id": 1, + "name": "groupPeer" + }, + { + "type": { + "type": "opt", + "childType": "string" + }, + "id": 2, + "name": "shortName" + } + ] + } + }, { "type": "rpc", "content": { @@ -9806,24 +10064,430 @@ "name": "SeqDate" }, "doc": [ - "Leaving group", + "Leaving group", + { + "type": "reference", + "argument": "groupPeer", + "category": "full", + "description": "Group's peer" + }, + { + "type": "reference", + "argument": "rid", + "category": "full", + "description": "Random Id of operation" + }, + { + "type": "reference", + "argument": "optimizations", + "category": "full", + "description": "Enabled Optimizations" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "GroupOutPeer" + }, + "id": 1, + "name": "groupPeer" + }, + { + "type": { + "type": "alias", + "childType": "randomId" + }, + "id": 2, + "name": "rid" + }, + { + "type": { + "type": "list", + "childType": { + "type": "enum", + "childType": "UpdateOptimization" + } + }, + "id": 3, + "name": "optimizations" + } + ] + } + }, + { + "type": "rpc", + "content": { + "name": "LeaveAndDelete", + "header": 2721, + "response": { + "type": "reference", + "name": "Seq" + }, + "doc": [ + "Leave group and Delete Chat", + { + "type": "reference", + "argument": "groupPeer", + "category": "full", + "description": "Group peer" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "GroupOutPeer" + }, + "id": 1, + "name": "groupPeer" + } + ] + } + }, + { + "type": "rpc", + "content": { + "name": "KickUser", + "header": 71, + "response": { + "type": "reference", + "name": "SeqDate" + }, + "doc": [ + "Kicking user from group", + { + "type": "reference", + "argument": "groupPeer", + "category": "full", + "description": "Group's peer" + }, + { + "type": "reference", + "argument": "user", + "category": "full", + "description": "users for removing" + }, + { + "type": "reference", + "argument": "rid", + "category": "full", + "description": "Random Id of operation" + }, + { + "type": "reference", + "argument": "optimizations", + "category": "full", + "description": "Enabled Optimizations" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "GroupOutPeer" + }, + "id": 1, + "name": "groupPeer" + }, + { + "type": { + "type": "alias", + "childType": "randomId" + }, + "id": 4, + "name": "rid" + }, + { + "type": { + "type": "struct", + "childType": "UserOutPeer" + }, + "id": 3, + "name": "user" + }, + { + "type": { + "type": "list", + "childType": { + "type": "enum", + "childType": "UpdateOptimization" + } + }, + "id": 5, + "name": "optimizations" + } + ] + } + }, + { + "type": "rpc", + "content": { + "name": "JoinGroupByPeer", + "header": 2722, + "response": { + "type": "reference", + "name": "Seq" + }, + "doc": [ + "Join group by peer", + { + "type": "reference", + "argument": "groupPeer", + "category": "full", + "description": "Group's peer" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "GroupOutPeer" + }, + "id": 1, + "name": "groupPeer" + } + ] + } + }, + { + "type": "comment", + "content": "Administration" + }, + { + "type": "rpc", + "content": { + "name": "MakeUserAdmin", + "header": 2784, + "response": { + "type": "reference", + "name": "SeqDate" + }, + "doc": [ + "Make user admin", + { + "type": "reference", + "argument": "groupPeer", + "category": "full", + "description": "Group's peer" + }, + { + "type": "reference", + "argument": "userPeer", + "category": "full", + "description": "User's peer" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "GroupOutPeer" + }, + "id": 1, + "name": "groupPeer" + }, + { + "type": { + "type": "struct", + "childType": "UserOutPeer" + }, + "id": 2, + "name": "userPeer" + } + ] + } + }, + { + "type": "rpc", + "content": { + "name": "DismissUserAdmin", + "header": 2791, + "response": { + "type": "reference", + "name": "Seq" + }, + "doc": [ + "Dismissing user admin", + { + "type": "reference", + "argument": "groupPeer", + "category": "full", + "description": "Group's peer" + }, + { + "type": "reference", + "argument": "userPeer", + "category": "full", + "description": "User's peer" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "GroupOutPeer" + }, + "id": 1, + "name": "groupPeer" + }, + { + "type": { + "type": "struct", + "childType": "UserOutPeer" + }, + "id": 2, + "name": "userPeer" + } + ] + } + }, + { + "type": "rpc", + "content": { + "name": "TransferOwnership", + "header": 2789, + "response": { + "type": "reference", + "name": "SeqDate" + }, + "doc": [ + "Transfer ownership of group", + { + "type": "reference", + "argument": "groupPeer", + "category": "full", + "description": "Group's peer" + }, + { + "type": "reference", + "argument": "newOwner", + "category": "full", + "description": "New group's owner" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "GroupOutPeer" + }, + "id": 1, + "name": "groupPeer" + }, + { + "type": { + "type": "struct", + "childType": "UserOutPeer" + }, + "id": 2, + "name": "newOwner" + } + ] + } + }, + { + "type": "struct", + "content": { + "name": "AdminSettings", + "doc": [ + "Admin Settings", + { + "type": "reference", + "argument": "showAdminsToMembers", + "category": "full", + "description": " Show admins in member list" + }, + { + "type": "reference", + "argument": "canMembersInvite", + "category": "full", + "description": " Can members of a group invite people" + }, + { + "type": "reference", + "argument": "canMembersEditGroupInfo", + "category": "full", + "description": " Can members edit group info" + }, + { + "type": "reference", + "argument": "canAdminsEditGroupInfo", + "category": "full", + "description": " Can admins edit group info" + }, + { + "type": "reference", + "argument": "showJoinLeaveMessages", + "category": "full", + "description": " Should join and leave messages be visible to members" + } + ], + "expandable": "true", + "attributes": [ + { + "type": "bool", + "id": 1, + "name": "showAdminsToMembers" + }, + { + "type": "bool", + "id": 2, + "name": "canMembersInvite" + }, + { + "type": "bool", + "id": 3, + "name": "canMembersEditGroupInfo" + }, + { + "type": "bool", + "id": 4, + "name": "canAdminsEditGroupInfo" + }, + { + "type": "bool", + "id": 5, + "name": "showJoinLeaveMessages" + } + ] + } + }, + { + "type": "rpc", + "content": { + "name": "LoadAdminSettings", + "header": 2790, + "response": { + "type": "anonymous", + "header": 2794, + "doc": [ + "Loaded settings", + { + "type": "reference", + "argument": "settings", + "category": "full", + "description": " Current group admin settings" + } + ], + "attributes": [ + { + "type": { + "type": "struct", + "childType": "AdminSettings" + }, + "id": 1, + "name": "settings" + } + ] + }, + "doc": [ + "Loading administration settings", { "type": "reference", "argument": "groupPeer", "category": "full", "description": "Group's peer" - }, - { - "type": "reference", - "argument": "rid", - "category": "full", - "description": "Random Id of operation" - }, - { - "type": "reference", - "argument": "optimizations", - "category": "full", - "description": "Enabled Optimizations" } ], "attributes": [ @@ -9834,25 +10498,6 @@ }, "id": 1, "name": "groupPeer" - }, - { - "type": { - "type": "alias", - "childType": "randomId" - }, - "id": 2, - "name": "rid" - }, - { - "type": { - "type": "list", - "childType": { - "type": "enum", - "childType": "UpdateOptimization" - } - }, - "id": 3, - "name": "optimizations" } ] } @@ -9860,37 +10505,25 @@ { "type": "rpc", "content": { - "name": "KickUser", - "header": 71, + "name": "SaveAdminSettings", + "header": 2792, "response": { "type": "reference", - "name": "SeqDate" + "name": "Void" }, "doc": [ - "Kicking user from group", + "Save administartion settings", { "type": "reference", "argument": "groupPeer", "category": "full", - "description": "Group's peer" - }, - { - "type": "reference", - "argument": "user", - "category": "full", - "description": "users for removing" - }, - { - "type": "reference", - "argument": "rid", - "category": "full", - "description": "Random Id of operation" + "description": "Group's Peer" }, { "type": "reference", - "argument": "optimizations", + "argument": "settings", "category": "full", - "description": "Enabled Optimizations" + "description": "Group's settings" } ], "attributes": [ @@ -9902,32 +10535,13 @@ "id": 1, "name": "groupPeer" }, - { - "type": { - "type": "alias", - "childType": "randomId" - }, - "id": 4, - "name": "rid" - }, { "type": { "type": "struct", - "childType": "UserOutPeer" - }, - "id": 3, - "name": "user" - }, - { - "type": { - "type": "list", - "childType": { - "type": "enum", - "childType": "UpdateOptimization" - } + "childType": "AdminSettings" }, - "id": 5, - "name": "optimizations" + "id": 2, + "name": "settings" } ] } @@ -9935,25 +10549,19 @@ { "type": "rpc", "content": { - "name": "MakeUserAdmin", - "header": 2784, + "name": "DeleteGroup", + "header": 2795, "response": { "type": "reference", - "name": "SeqDate" + "name": "Seq" }, "doc": [ - "Make user admin", + "Delete Group", { "type": "reference", "argument": "groupPeer", "category": "full", "description": "Group's peer" - }, - { - "type": "reference", - "argument": "userPeer", - "category": "full", - "description": "User's peer" } ], "attributes": [ @@ -9964,14 +10572,6 @@ }, "id": 1, "name": "groupPeer" - }, - { - "type": { - "type": "struct", - "childType": "UserOutPeer" - }, - "id": 2, - "name": "userPeer" } ] } @@ -9979,25 +10579,19 @@ { "type": "rpc", "content": { - "name": "TransferOwnership", - "header": 2789, + "name": "ShareHistory", + "header": 2796, "response": { "type": "reference", - "name": "SeqDate" + "name": "Seq" }, "doc": [ - "Transfer ownership of group", + "Share History", { "type": "reference", "argument": "groupPeer", "category": "full", "description": "Group's peer" - }, - { - "type": "reference", - "argument": "newOwner", - "category": "full", - "description": "New group's owner" } ], "attributes": [ @@ -10008,14 +10602,6 @@ }, "id": 1, "name": "groupPeer" - }, - { - "type": { - "type": "alias", - "childType": "userId" - }, - "id": 2, - "name": "newOwner" } ] } @@ -11926,39 +12512,9 @@ }, { "type": "reference", - "argument": "title", - "category": "full", - "description": " Peer title" - }, - { - "type": "reference", - "argument": "description", + "argument": "optMatchString", "category": "full", "description": " Description" - }, - { - "type": "reference", - "argument": "membersCount", - "category": "full", - "description": " Members count" - }, - { - "type": "reference", - "argument": "dateCreated", - "category": "full", - "description": " Group Creation Date" - }, - { - "type": "reference", - "argument": "creator", - "category": "full", - "description": " Group Creator uid" - }, - { - "type": "reference", - "argument": "isPublic", - "category": "full", - "description": " Is group public" } ], "attributes": [ @@ -11970,61 +12526,13 @@ "id": 1, "name": "peer" }, - { - "type": "string", - "id": 2, - "name": "title" - }, { "type": { "type": "opt", "childType": "string" }, "id": 3, - "name": "description" - }, - { - "type": { - "type": "opt", - "childType": "int32" - }, - "id": 4, - "name": "membersCount" - }, - { - "type": { - "type": "opt", - "childType": { - "type": "alias", - "childType": "date" - } - }, - "id": 5, - "name": "dateCreated" - }, - { - "type": { - "type": "opt", - "childType": "int32" - }, - "id": 6, - "name": "creator" - }, - { - "type": { - "type": "opt", - "childType": "bool" - }, - "id": 7, - "name": "isPublic" - }, - { - "type": { - "type": "opt", - "childType": "bool" - }, - "id": 8, - "name": "isJoined" + "name": "optMatchString" } ] } @@ -19066,7 +19574,7 @@ { "type": "reference", "argument": "keyGroupId", - "category": "hidden", + "category": "full", "description": " Key Group Id" } ], @@ -19090,6 +19598,45 @@ ] } }, + { + "type": "struct", + "content": { + "name": "KeyGroupHolder", + "doc": [ + "Key Group Holder", + { + "type": "reference", + "argument": "uid", + "category": "full", + "description": " User's id" + }, + { + "type": "reference", + "argument": "keyGroup", + "category": "full", + "description": " Key Group" + } + ], + "attributes": [ + { + "type": { + "type": "alias", + "childType": "userId" + }, + "id": 1, + "name": "uid" + }, + { + "type": { + "type": "struct", + "childType": "EncryptionKeyGroup" + }, + "id": 2, + "name": "keyGroup" + } + ] + } + }, { "type": "rpc", "content": { @@ -19099,18 +19646,6 @@ "type": "anonymous", "header": 2664, "doc": [ - { - "type": "reference", - "argument": "seq", - "category": "full", - "description": " seq" - }, - { - "type": "reference", - "argument": "state", - "category": "full", - "description": " state" - }, { "type": "reference", "argument": "date", @@ -19121,7 +19656,7 @@ "type": "reference", "argument": "obsoleteKeyGroups", "category": "full", - "description": " obsolete key groups" + "description": " obsolete key group ids" }, { "type": "reference", @@ -19131,25 +19666,6 @@ } ], "attributes": [ - { - "type": { - "type": "opt", - "childType": "int32" - }, - "id": 1, - "name": "seq" - }, - { - "type": { - "type": "opt", - "childType": { - "type": "alias", - "childType": "seq_state" - } - }, - "id": 2, - "name": "state" - }, { "type": { "type": "opt", @@ -19158,7 +19674,7 @@ "childType": "date" } }, - "id": 3, + "id": 1, "name": "date" }, { @@ -19169,7 +19685,7 @@ "childType": "KeyGroupId" } }, - "id": 4, + "id": 2, "name": "obsoleteKeyGroups" }, { @@ -19177,10 +19693,10 @@ "type": "list", "childType": { "type": "struct", - "childType": "KeyGroupId" + "childType": "KeyGroupHolder" } }, - "id": 5, + "id": 3, "name": "missedKeyGroups" } ] @@ -21273,4 +21789,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/actor-server/actor-core/src/main/protobuf/dialog.proto b/actor-server/actor-core/src/main/protobuf/dialog.proto index 0910ff52ba..0346d683ea 100644 --- a/actor-server/actor-core/src/main/protobuf/dialog.proto +++ b/actor-server/actor-core/src/main/protobuf/dialog.proto @@ -135,6 +135,14 @@ message DialogRootCommands { Peer dest = 1; int64 client_auth_id = 2; } + + message Bump { + option (scalapb.message).extends = "im.actor.server.dialog.DialogRootCommand"; + + Peer dest = 1; + } + + message BumpAck {} } message DialogRootQueries { @@ -200,6 +208,7 @@ message DialogRootEnvelope { DialogRootCommands.Favourite favourite = 7; DialogRootCommands.Unfavourite unfavourite = 8; DialogRootCommands.Delete delete = 9; + DialogRootCommands.Bump bump = 12; } } @@ -259,7 +268,9 @@ message DialogCommands { google.protobuf.StringValue delivery_tag = 13; } - message SendMessageAck {} + message SendMessageAck { + Peer updated_sender = 1; + } message MessageReceived { option (scalapb.message).extends = "im.actor.server.dialog.DirectDialogCommand"; diff --git a/actor-server/actor-core/src/main/protobuf/globalname.proto b/actor-server/actor-core/src/main/protobuf/globalname.proto new file mode 100644 index 0000000000..caadea04eb --- /dev/null +++ b/actor-server/actor-core/src/main/protobuf/globalname.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package im.actor.server.names; + +option (scalapb.options) = { + flat_package: true +}; + + +import "scalapb/scalapb.proto"; + +enum OwnerType { + User = 0; + Group = 1; +} + +message GlobalNameOwner { + OwnerType owner_type = 1; + int32 owner_id = 2; +} diff --git a/actor-server/actor-core/src/main/protobuf/group.proto b/actor-server/actor-core/src/main/protobuf/group.proto index 5092545e6a..183b00c31d 100644 --- a/actor-server/actor-core/src/main/protobuf/group.proto +++ b/actor-server/actor-core/src/main/protobuf/group.proto @@ -12,7 +12,6 @@ import "file.proto"; enum GroupType { General = 1; - Public = 2; Channel = 3; } @@ -98,6 +97,13 @@ message GroupEvents { optional string description = 1; } + message ShortNameUpdated { + option (scalapb.message).extends = "im.actor.server.group.GroupEvent"; + + required int64 ts = 1 [(scalapb.field).type = "java.time.Instant"]; + optional string short_name = 2; + } + message TopicUpdated { option (scalapb.message).extends = "im.actor.server.group.GroupEvent"; @@ -105,6 +111,7 @@ message GroupEvents { optional string topic = 1; } + // deprecated in favour of AdminStatusChanged message UserBecameAdmin { option (scalapb.message).extends = "im.actor.server.group.GroupEvent"; @@ -113,6 +120,14 @@ message GroupEvents { required int32 promoter_user_id = 2; } + message AdminStatusChanged { + option (scalapb.message).extends = "im.actor.server.group.GroupEvent"; + + required int64 ts = 1 [(scalapb.field).type = "java.time.Instant"]; + required int32 user_id = 2; + required bool is_admin = 3; + } + message IntegrationTokenRevoked { option (scalapb.message).extends = "im.actor.server.group.GroupEvent"; @@ -126,4 +141,46 @@ message GroupEvents { required int64 ts = 1 [(scalapb.field).type = "java.time.Instant"]; required int32 user_id = 2; } + + message AdminSettingsUpdated { + option (scalapb.message).extends = "im.actor.server.group.GroupEvent"; + + required int64 ts = 1 [(scalapb.field).type = "java.time.Instant"]; + required int32 settings_bit_mask = 2; + } + + message HistoryBecameShared { + option (scalapb.message).extends = "im.actor.server.group.GroupEvent"; + + required int64 ts = 1 [(scalapb.field).type = "java.time.Instant"]; + required int32 executor_user_id = 2; + } + + message MembersBecameAsync { + option (scalapb.message).extends = "im.actor.server.group.GroupEvent"; + + required int64 ts = 1 [(scalapb.field).type = "java.time.Instant"]; + } + + message GroupDeleted { + option (scalapb.message).extends = "im.actor.server.group.GroupEvent"; + + required int64 ts = 1 [(scalapb.field).type = "java.time.Instant"]; + required int32 executor_user_id = 2; + } + + message ExtAdded { + option (scalapb.message).extends = "im.actor.server.group.GroupEvent"; + + required int64 ts = 1 [(scalapb.field).type = "java.time.Instant"]; + required bytes ext = 2 [(scalapb.field).type = "im.actor.server.group.GroupExt"]; + } + + message ExtRemoved { + option (scalapb.message).extends = "im.actor.server.group.GroupEvent"; + + required int64 ts = 1 [(scalapb.field).type = "java.time.Instant"]; + required string key = 2; + } + } diff --git a/actor-server/actor-core/src/main/protobuf/groupV2.proto b/actor-server/actor-core/src/main/protobuf/groupV2.proto index f00627bb63..943b5afd55 100644 --- a/actor-server/actor-core/src/main/protobuf/groupV2.proto +++ b/actor-server/actor-core/src/main/protobuf/groupV2.proto @@ -18,6 +18,22 @@ import "file.proto"; import "sequence.proto"; import "dialog.proto"; +// TODO: Can't share with UserExt: proto 2/3 +message GroupExt { + string key = 1; + oneof value { + string string_value = 2; + bool bool_value = 3; + } +} + +message GroupMember { + int32 user_id = 1; + int32 inviter_user_id = 2; + int64 invited_at = 3; + bool is_admin = 4; +} + message GroupEnvelope { int32 group_id = 1; oneof command { @@ -30,22 +46,30 @@ message GroupEnvelope { GroupCommands.UpdateTitle update_title = 8; GroupCommands.UpdateTopic update_topic = 9; GroupCommands.UpdateAbout update_about = 10; -// GroupCommands.MakePublic make_public = 11; + GroupCommands.UpdateShortName update_short_name = 27; + GroupCommands.MakeHistoryShared make_history_shared = 32; GroupCommands.RevokeIntegrationToken revoke_token = 12; GroupCommands.MakeUserAdmin make_user_admin = 13; + GroupCommands.DismissUserAdmin dismiss_user_admin = 28; GroupCommands.TransferOwnership transfer_ownership = 14; + GroupCommands.UpdateAdminSettings update_admin_settings = 30; + GroupCommands.DeleteGroup delete_group = 33; + GroupCommands.AddExt add_ext = 34; + GroupCommands.RemoveExt remove_ext = 35; } oneof query { GroupQueries.GetAccessHash get_access_hash = 15; GroupQueries.GetTitle get_title = 16; GroupQueries.GetIntegrationToken get_integration_token = 17; - GroupQueries.GetMembers get_members = 18; + GroupQueries.GetMembers get_members = 18; // internalGetMembers GroupQueries.LoadMembers load_members = 19; - GroupQueries.IsPublic is_public = 20; + GroupQueries.IsChannel is_channel = 31; GroupQueries.IsHistoryShared is_history_shared = 21; GroupQueries.GetApiStruct get_api_struct = 22; GroupQueries.GetApiFullStruct get_api_full_struct = 23; GroupQueries.CheckAccessHash check_access_hash = 24; + GroupQueries.CanSendMessage can_send_message = 26; + GroupQueries.LoadAdminSettings load_admin_settings = 29; } DialogEnvelope dialog_envelope = 25; } @@ -123,16 +147,7 @@ message GroupCommands { string title = 3; int64 random_id = 4; } -// -// message MakePublic { -// option (scalapb.message).extends = "im.actor.server.group.GroupCommand"; -// -// required int32 group_id = 1; -// optional string descrption = 2; -// } -// -// message MakePublicAck {} -// + message UpdateTopic { option (scalapb.message).extends = "GroupCommand"; @@ -151,6 +166,14 @@ message GroupCommands { int64 random_id = 4; } + message UpdateShortName { + option (scalapb.message).extends = "GroupCommand"; + + int32 client_user_id = 1; + int64 client_auth_id = 2; + google.protobuf.StringValue short_name = 3; + } + message MakeUserAdmin { option (scalapb.message).extends = "GroupCommand"; @@ -159,6 +182,14 @@ message GroupCommands { int32 candidate_user_id = 3; } + message DismissUserAdmin { + option (scalapb.message).extends = "GroupCommand"; + + int32 client_user_id = 1; + int64 client_auth_id = 2; + int32 target_user_id = 3; + } + message RevokeIntegrationToken { option (scalapb.message).extends = "GroupCommand"; @@ -176,6 +207,46 @@ message GroupCommands { int64 client_auth_id = 3; int32 new_owner_id = 4; } + + message UpdateAdminSettings { + option (scalapb.message).extends = "GroupCommand"; + + int32 client_user_id = 1; + int32 settings_bit_mask = 2; + } + + message UpdateAdminSettingsAck {} + + message MakeHistoryShared { + option (scalapb.message).extends = "GroupCommand"; + + int32 client_user_id = 1; + int64 client_auth_id = 2; + } + + message DeleteGroup { + option (scalapb.message).extends = "GroupCommand"; + + int32 client_user_id = 1; + int64 client_auth_id = 2; + } + + message AddExt { + option (scalapb.message).extends = "GroupCommand"; + + GroupExt ext = 1; + } + + message AddExtAck {} + + message RemoveExt { + option (scalapb.message).extends = "GroupCommand"; + + string key = 1; + } + + message RemoveExtAck {} + } message GroupQueries { @@ -193,6 +264,7 @@ message GroupQueries { option (scalapb.message).extends = "GroupQuery"; int32 client_user_id = 1; + bool load_group_members = 2; } message GetApiStructResponse { @@ -229,12 +301,12 @@ message GroupQueries { google.protobuf.Int32Value bot_id = 3; } - message IsPublic { + message IsChannel { option (scalapb.message).extends = "GroupQuery"; } - message IsPublicResponse { - bool is_public = 1; + message IsChannelResponse { + bool is_channel = 1; } message IsHistoryShared { @@ -270,7 +342,30 @@ message GroupQueries { } message LoadMembersResponse { - repeated int32 user_ids = 1; + repeated GroupMember members = 1; google.protobuf.BytesValue offset = 2;// should it be Option[Array[Byte]] } + + message CanSendMessage { + option (scalapb.message).extends = "GroupQuery"; + + int32 client_user_id = 1; + } + + message CanSendMessageResponse { + bool can_send = 1; + bool is_channel = 2; + repeated int32 member_ids = 3; + google.protobuf.Int32Value bot_id = 4; + } + + message LoadAdminSettings { + option (scalapb.message).extends = "GroupQuery"; + + int32 client_user_id = 1; + } + + message LoadAdminSettingsResponse { + bytes settings = 1 [(scalapb.field).type = "im.actor.api.rpc.groups.ApiAdminSettings"]; + } } diff --git a/actor-server/actor-core/src/main/protobuf/sequence.proto b/actor-server/actor-core/src/main/protobuf/sequence.proto index c1203289aa..056b93a2f2 100644 --- a/actor-server/actor-core/src/main/protobuf/sequence.proto +++ b/actor-server/actor-core/src/main/protobuf/sequence.proto @@ -26,6 +26,7 @@ message PushData { string text = 1; string censoredText = 3; Peer peer = 2; + bool is_mentioned = 4; } message PushRules { @@ -87,9 +88,10 @@ message UserSequenceCommands { option (scalapb.message).extends = "im.actor.server.sequence.VendorPushCommand"; oneof creds { - GooglePushCredentials google = 1; + GCMPushCredentials gcm = 1; ApplePushCredentials apple = 2; ActorPushCredentials actor = 3; + FirebasePushCredentials firebase = 4; } } @@ -97,9 +99,10 @@ message UserSequenceCommands { option (scalapb.message).extends = "im.actor.server.sequence.VendorPushCommand"; oneof creds { - GooglePushCredentials google = 1; + GCMPushCredentials gcm = 1; ApplePushCredentials apple = 2; ActorPushCredentials actor = 3; + FirebasePushCredentials firebase = 4; } } diff --git a/actor-server/actor-core/src/main/resources/reference.conf b/actor-server/actor-core/src/main/resources/reference.conf index 55d3a0e650..7510ed83bc 100644 --- a/actor-server/actor-core/src/main/resources/reference.conf +++ b/actor-server/actor-core/src/main/resources/reference.conf @@ -27,9 +27,10 @@ services { } google { - push { - max-connections: 20 - + gcm { + keys: [] + } + firebase { keys: [] } } diff --git a/actor-server/actor-core/src/main/scala/im/actor/api/rpc.scala b/actor-server/actor-core/src/main/scala/im/actor/api/rpc.scala index 66cb42fe6d..3647567cd9 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/api/rpc.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/api/rpc.scala @@ -3,7 +3,7 @@ package im.actor.api import akka.http.scaladsl.util.FastFuture import cats.data.Xor import im.actor.server.CommonErrors -import im.actor.server.group.GroupErrors.GroupNotFound +import im.actor.server.group.GroupErrors.{ GroupAlreadyDeleted, GroupNotFound } import im.actor.server.office.EntityNotFoundError import im.actor.server.user.UserErrors.UserNotFound @@ -16,6 +16,7 @@ package object rpc extends PeersImplicits with HistoryImplicits with DialogConve object CommonRpcErrors { val GroupNotFound = RpcError(404, "GROUP_NOT_FOUND", "", false, None) + val GroupDeleted = RpcError(404, "GROUP_DELETED", "", false, None) val InvalidAccessHash = RpcError(403, "INVALID_ACCESS_HASH", "", false, None) val UnsupportedRequest = RpcError(400, "REQUEST_NOT_SUPPORTED", "Operation not supported.", false, None) val UserNotAuthorized = RpcError(403, "USER_NOT_AUTHORIZED", "", false, None) @@ -23,14 +24,15 @@ package object rpc extends PeersImplicits with HistoryImplicits with DialogConve val UserPhoneNotFound = RpcError(404, "USER_PHONE_NOT_FOUND", "", false, None) val EntityNotFound = RpcError(404, "ENTITY_NOT_FOUND", "", false, None) val NotSupportedInOss = RpcError(400, "NOT_SUPPORTED_IN_OSS", "Feature is not supported in the Open-Source version.", canTryAgain = false, None) - val IntenalError = RpcError(500, "INTERNAL_ERROR", "", false, None) + val InternalError = RpcError(500, "INTERNAL_ERROR", "", false, None) def forbidden(userMessage: String = "You are not allowed to do this.") = RpcError(403, "FORBIDDEN", userMessage, false, None) } def recoverCommon: PartialFunction[Throwable, RpcError] = { - case UserNotFound(_) ⇒ CommonRpcErrors.UserNotFound - case GroupNotFound(_) ⇒ CommonRpcErrors.GroupNotFound + case _: UserNotFound ⇒ CommonRpcErrors.UserNotFound + case _: GroupNotFound ⇒ CommonRpcErrors.GroupNotFound + case _: GroupAlreadyDeleted ⇒ CommonRpcErrors.GroupDeleted case EntityNotFoundError ⇒ CommonRpcErrors.EntityNotFound case CommonErrors.Forbidden(message) ⇒ CommonRpcErrors.forbidden(message) } diff --git a/actor-server/actor-core/src/main/scala/im/actor/api/rpc/DBIOResultRpc.scala b/actor-server/actor-core/src/main/scala/im/actor/api/rpc/DBIOResultRpc.scala index 27da70c4c2..bf450376cc 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/api/rpc/DBIOResultRpc.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/api/rpc/DBIOResultRpc.scala @@ -17,6 +17,7 @@ object DBIOResultRpc { def pure[A](a: A): DBIO[A] = DBIO.successful(a) def flatMap[A, B](fa: DBIO[A])(f: A ⇒ DBIO[B]): DBIO[B] = fa flatMap f override def map[A, B](fa: DBIO[A])(f: A ⇒ B): DBIO[B] = fa map f + def tailRecM[A, B](a: A)(f: A ⇒ DBIO[Either[A, B]]): DBIO[B] = defaultTailRecM(a)(f) } def point[A](a: A): Result[A] = Result[A](DBIO.successful(right(a))) diff --git a/actor-server/actor-core/src/main/scala/im/actor/api/rpc/MaybeAuthorized.scala b/actor-server/actor-core/src/main/scala/im/actor/api/rpc/MaybeAuthorized.scala index 0262b81910..8da305b930 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/api/rpc/MaybeAuthorized.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/api/rpc/MaybeAuthorized.scala @@ -36,11 +36,9 @@ case object MaybeAuthorized extends MaybeAuthorizedInstances trait MaybeAuthorizedInstances { implicit val maybeAuthorizedInstance = new Functor[MaybeAuthorized] with Monad[MaybeAuthorized] { - override def map[A, B](fa: MaybeAuthorized[A])(f: A ⇒ B): MaybeAuthorized[B] = fa.map(f) - def pure[A](a: A): MaybeAuthorized[A] = Authorized(a) - def flatMap[A, B](fa: MaybeAuthorized[A])(f: A ⇒ MaybeAuthorized[B]): MaybeAuthorized[B] = fa.flatMap(f) + def tailRecM[A, B](a: A)(f: A ⇒ MaybeAuthorized[Either[A, B]]): MaybeAuthorized[B] = defaultTailRecM(a)(f) } } diff --git a/actor-server/actor-core/src/main/scala/im/actor/api/rpc/PeersImplicits.scala b/actor-server/actor-core/src/main/scala/im/actor/api/rpc/PeersImplicits.scala index d9d0e1b5ee..f8a868c348 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/api/rpc/PeersImplicits.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/api/rpc/PeersImplicits.scala @@ -24,6 +24,9 @@ trait PeersImplicits { lazy val asPeer: ApiPeer = ApiPeer(ApiPeerType.Group, groupOutPeer.groupId) + + lazy val asModel: Peer = + Peer(PeerType.Group, groupOutPeer.groupId) } implicit class ExtPeerModel(model: Peer) { diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/CommonErrors.scala b/actor-server/actor-core/src/main/scala/im/actor/server/CommonErrors.scala index cd654cbebf..b13311bf09 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/CommonErrors.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/CommonErrors.scala @@ -3,4 +3,4 @@ package im.actor.server object CommonErrors { case class Forbidden(message: String) extends RuntimeException(message) object Forbidden extends Forbidden("You are not allowed to do this.") -} \ No newline at end of file +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/api/TypeMappers.scala b/actor-server/actor-core/src/main/scala/im/actor/server/api/TypeMappers.scala index 1c266761fd..49c441821e 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/api/TypeMappers.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/api/TypeMappers.scala @@ -9,7 +9,7 @@ import com.google.protobuf.wrappers.{ BytesValue, Int64Value } import com.google.protobuf.{ ByteString, CodedInputStream } import com.trueaccord.scalapb.TypeMapper import im.actor.api.rpc.files.ApiAvatar -import im.actor.api.rpc.groups.{ ApiGroup, ApiGroupFull } +import im.actor.api.rpc.groups.{ ApiAdminSettings, ApiGroup, ApiGroupFull } import im.actor.api.rpc.messaging.ApiMessage import im.actor.api.rpc.misc.ApiExtension import im.actor.api.rpc.peers.ApiPeer @@ -17,6 +17,7 @@ import im.actor.api.rpc.sequence.SeqUpdate import im.actor.api.rpc.users.ApiSex.ApiSex import im.actor.api.rpc.users.{ ApiFullUser, ApiUser, ApiSex ⇒ S } import im.actor.serialization.ActorSerializer +import im.actor.server.group.GroupExt import org.joda.time.DateTime abstract class ActorSystemMapper[BaseType, CustomType](implicit val system: ActorSystem) extends TypeMapper[BaseType, CustomType] @@ -184,6 +185,29 @@ private[api] trait MessageMapper { def unapplyExtension(ext: ApiExtension): ByteString = ByteString.copyFrom(ext.toByteArray) + def applyGroupExt(bytes: ByteString): GroupExt = { + if (bytes.size > 0) { + GroupExt.parseFrom(CodedInputStream.newInstance(bytes.asReadOnlyByteBuffer())) + } else { + null + } + } + + def unapplyGroupExt(ext: GroupExt): ByteString = + ByteString.copyFrom(ext.toByteArray) + + private def applyAdminSettings(bytes: ByteString): ApiAdminSettings = { + if (bytes.size() > 0) { + val res = ApiAdminSettings.parseFrom(CodedInputStream.newInstance(bytes.toByteArray)) + get(res) + } else { + null + } + } + + private def unapplyAdminSettings(settings: ApiAdminSettings): ByteString = + ByteString.copyFrom(settings.toByteArray) + implicit val seqUpdMapper: TypeMapper[ByteString, SeqUpdate] = TypeMapper(applySeqUpdate)(unapplySeqUpdate) implicit val anyRefMapper: TypeMapper[ByteString, AnyRef] = TypeMapper(applyAnyRef)(unapplyAnyRef) @@ -216,6 +240,10 @@ private[api] trait MessageMapper { implicit val extensionMapper: TypeMapper[ByteString, ApiExtension] = TypeMapper(applyExtension)(unapplyExtension) + implicit val groupExtMapper: TypeMapper[ByteString, GroupExt] = TypeMapper(applyGroupExt)(unapplyGroupExt) + + implicit val adminSettingsMapper: TypeMapper[ByteString, ApiAdminSettings] = TypeMapper(applyAdminSettings)(unapplyAdminSettings) + implicit def actorRefMapper(implicit system: ActorSystem): TypeMapper[String, ActorRef] = new ActorSystemMapper[String, ActorRef]() { override def toCustom(base: String): ActorRef = diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/cqrs/Processor.scala b/actor-server/actor-core/src/main/scala/im/actor/server/cqrs/Processor.scala index 97ec86eda5..4739346df5 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/cqrs/Processor.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/cqrs/Processor.scala @@ -65,6 +65,7 @@ trait ProcessorStateControl[S <: ProcessorState[S]] { protected def getInitialState: S + //TODO: rename to processorState final def state: S = _state def setState(state: S) = this._state = state diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/ActorDelivery.scala b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/ActorDelivery.scala index e82dbaee32..d34c550516 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/ActorDelivery.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/ActorDelivery.scala @@ -1,11 +1,9 @@ package im.actor.server.dialog import akka.actor.ActorSystem -import akka.http.scaladsl.util.FastFuture import im.actor.api.rpc.PeersImplicits import im.actor.api.rpc.counters.{ ApiAppCounters, UpdateCountersChanged } import im.actor.api.rpc.messaging._ -import im.actor.server.db.DbExtension import im.actor.server.messaging.PushText import im.actor.server.model.Peer import im.actor.server.sequence.{ PushData, PushRules, SeqState, SeqUpdatesExtension } @@ -14,8 +12,8 @@ import im.actor.server.user.UserExtension import scala.concurrent.{ ExecutionContext, Future } //default extension -final class ActorDelivery()(implicit val system: ActorSystem) - extends DeliveryExtension +final class ActorDelivery(val system: ActorSystem) + extends DeliveryExtension(system, Array.emptyByteArray) with PushText with PeersImplicits { @@ -42,6 +40,11 @@ final class ActorDelivery()(implicit val system: ActorSystem) quotedMessage = None ) + val isMentioned = message match { + case ApiTextMessage(_, mentions, _) ⇒ mentions.contains(receiverUserId) + case _ ⇒ false + } + for { senderName ← UserExtension(system).getName(senderUserId, receiverUserId) (pushText, censoredPushText) ← getPushText(peer, receiverUserId, senderName, message) @@ -53,8 +56,9 @@ final class ActorDelivery()(implicit val system: ActorSystem) .withText(pushText) .withCensoredText(censoredPushText) .withPeer(peer) + .withIsMentioned(isMentioned) ), - deliveryId = deliveryId(peer, randomId), + deliveryId = seqUpdExt.msgDeliveryId(peer, randomId), deliveryTag = deliveryTag ) } yield () @@ -100,7 +104,7 @@ final class ActorDelivery()(implicit val system: ActorSystem) default = Some(senderUpdate), custom = senderAuthId map (authId ⇒ Map(authId → senderClientUpdate)) getOrElse Map.empty, pushRules = PushRules(isFat = isFat, excludeAuthIds = senderAuthId.toSeq), - deliveryId = deliveryId(peer, randomId), + deliveryId = seqUpdExt.msgDeliveryId(peer, randomId), deliveryTag = deliveryTag ) } @@ -134,9 +138,6 @@ final class ActorDelivery()(implicit val system: ActorSystem) ) } yield () - private def deliveryId(peer: Peer, randomId: Long) = - s"msg_${peer.`type`.value}_${peer.id}_${randomId}" - private def reduceKey(prefix: String, peer: Peer): String = s"${prefix}_${peer.`type`.value}_${peer.id}" diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DeliveryExtension.scala b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DeliveryExtension.scala index 3d48b5970e..2c0d842df6 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DeliveryExtension.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DeliveryExtension.scala @@ -1,12 +1,13 @@ package im.actor.server.dialog +import akka.actor.ActorSystem import im.actor.api.rpc.messaging.ApiMessage import im.actor.server.sequence.SeqState import im.actor.server.model.Peer import scala.concurrent.Future -trait DeliveryExtension { +abstract class DeliveryExtension(system: ActorSystem, extData: Array[Byte]) { def receiverDelivery( receiverUserId: Int, diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogCommandHandlers.scala b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogCommandHandlers.scala index 478c984b2a..bd06349b2f 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogCommandHandlers.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogCommandHandlers.scala @@ -5,7 +5,6 @@ import java.time.Instant import akka.actor.Status import akka.http.scaladsl.util.FastFuture import akka.pattern.pipe -import com.google.protobuf.wrappers.Int64Value import im.actor.api.rpc.PeersImplicits import im.actor.api.rpc.messaging._ import im.actor.server.ApiConversions._ @@ -17,7 +16,6 @@ import im.actor.server.pubsub.{ PeerMessage, PubSubExtension } import im.actor.server.sequence.{ SeqState, SeqStateDate } import im.actor.server.social.SocialManager import im.actor.util.cache.CacheHelpers._ -import org.joda.time.DateTime import scala.concurrent.Future import scala.util.Failure @@ -42,7 +40,7 @@ trait DialogCommandHandlers extends PeersImplicits with UserAcl { replyTo forward fail context.become(receiveCommand) unstashAll() - }: Receive) orElse reactions, discardOld = true) + }: Receive) orElse reactions orElse queries, discardOld = true) val optClientAuthId = sm.senderAuthId withValidAccessHash(sm.getDest, optClientAuthId, sm.accessHash) { @@ -57,16 +55,31 @@ trait DialogCommandHandlers extends PeersImplicits with UserAcl { case true ⇒ FastFuture.failed(NotUniqueRandomId) case false ⇒ withNonBlockedPeer[SeqStateDate](userId, sm.getDest)( - default = for { - _ ← dialogExt.ackSendMessage(peer, sm.copy(date = Some(sendDate))) - _ ← db.run(writeHistoryMessage(selfPeer, peer, new DateTime(sendDate), sm.randomId, message.header, message.toByteArray)) - //_ = dialogExt.updateCounters(peer, userId) - SeqState(seq, state) ← deliveryExt.senderDelivery(userId, optClientAuthId, peer, sm.randomId, sendDate, message, sm.isFat, sm.deliveryTag) - } yield SeqStateDate(seq, state, sendDate), - failed = for { - _ ← db.run(writeHistoryMessageSelf(userId, peer, userId, new DateTime(sendDate), sm.randomId, message.header, message.toByteArray)) - SeqState(seq, state) ← deliveryExt.senderDelivery(userId, optClientAuthId, peer, sm.randomId, sendDate, message, sm.isFat, sm.deliveryTag) - } yield SeqStateDate(seq, state, sendDate) + default = + for { + SendMessageAck(updatedSender) ← dialogExt.ackSendMessage(peer, sm.withDate(sendDate)) + finalPeer = updatedSender getOrElse selfPeer + _ ← db.run(writeHistoryMessage(finalPeer, peer, sendDate, sm.randomId, message.header, message.toByteArray)) + //_ = dialogExt.updateCounters(peer, userId) + _ ← dialogExt.bump(userId, peer) + SeqState(seq, state) ← deliveryExt.senderDelivery( + senderUserId = userId, + senderAuthId = optClientAuthId, + peer = peer, + randomId = sm.randomId, + timestamp = sendDate, + message = message, + isFat = sm.isFat, + deliveryTag = sm.deliveryTag + ) + } yield SeqStateDate(seq, state, sendDate), + failed = + for { + _ ← db.run(writeHistoryMessageSelf(userId, peer, userId, sendDate, sm.randomId, message.header, message.toByteArray)) + SeqState(seq, state) ← deliveryExt.senderDelivery(userId, optClientAuthId, peer, sm.randomId, sendDate, message, sm.isFat, sm.deliveryTag) + } yield { + SeqStateDate(seq, state, sendDate) + } ) } } yield seqStateDate @@ -87,10 +100,19 @@ trait DialogCommandHandlers extends PeersImplicits with UserAcl { SocialManager.recordRelation(userId, sm.getOrigin.id) } - deliveryExt - .receiverDelivery(userId, sm.getOrigin.id, peer, sm.randomId, messageDate, sm.message, sm.isFat, sm.deliveryTag) - .map(_ ⇒ SendMessageAck()) - .pipeTo(sender()) + (for { + _ ← deliveryExt.receiverDelivery( + receiverUserId = userId, + senderUserId = sm.getOrigin.id, + peer = peer, + randomId = sm.randomId, + timestamp = messageDate, + message = sm.message, + isFat = sm.isFat, + deliveryTag = sm.deliveryTag + ) + _ ← dialogExt.bump(userId, peer) + } yield SendMessageAck()) pipeTo sender() deliveryExt.sendCountersUpdate(userId) } @@ -107,8 +129,18 @@ trait DialogCommandHandlers extends PeersImplicits with UserAcl { } else { persist(NewMessage(randomId, Instant.ofEpochMilli(dateMillis), senderUserId, message.header)) { e ⇒ commit(e) - db.run(writeHistoryMessageSelf(userId, peer, senderUserId, new DateTime(dateMillis), randomId, message.header, message.toByteArray)) - .map(_ ⇒ WriteMessageSelfAck()) pipeTo sender() + (for { + _ ← dialogExt.bump(userId, peer) + _ ← db.run(writeHistoryMessageSelf( + userId, + peer, + senderUserId, + dateMillis, + randomId, + message.header, + message.toByteArray + )) + } yield WriteMessageSelfAck()) pipeTo sender() } } } diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogExtension.scala b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogExtension.scala index f90749ab58..4a06fcf58c 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogExtension.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogExtension.scala @@ -15,6 +15,7 @@ import im.actor.api.rpc.peers.ApiPeer import im.actor.extension.InternalExtensions import im.actor.server.db.DbExtension import im.actor.server.dialog.DialogCommands._ +import im.actor.server.dialog.DialogRootCommands.BumpAck import im.actor.server.group.{ GroupEnvelope, GroupExtension } import im.actor.server.model._ import im.actor.server.persist.HistoryMessageRepo @@ -100,7 +101,7 @@ final class DialogExtensionImpl(system: ActorSystem) extends DialogExtension wit senderUserId = senderUserId, sendMessage = SendMessage( origin = Some(Peer.privat(senderUserId)), - dest = Some(peer.asModel), + dest = Some(mPeer), senderAuthId = None, date = None, randomId = randomId, @@ -152,28 +153,29 @@ final class DialogExtensionImpl(system: ActorSystem) extends DialogExtension wit } yield seqStateDate.copy(seq = seq, state = state) } - def ackSendMessage(peer: Peer, sm: SendMessage): Future[Unit] = - (processorRegion(peer) ? envelope(peer, DialogEnvelope().withSendMessage(sm))).mapTo[SendMessageAck] map (_ ⇒ ()) - - def writeMessage( - peer: ApiPeer, - senderUserId: Int, - date: Instant, - randomId: Long, - message: ApiMessage - ): Future[Unit] = - withValidPeer(peer.asModel, senderUserId, failed = FastFuture.successful(())) { - for { - memberIds ← fetchMemberIds(DialogId(peer.asModel, senderUserId)) - _ ← Future.sequence(memberIds map (writeMessageSelf(_, peer, senderUserId, new DateTime(date.toEpochMilli), randomId, message))) - } yield () - } + def ackSendMessage(peer: Peer, sm: SendMessage): Future[SendMessageAck] = + (processorRegion(peer) ? envelope(peer, DialogEnvelope().withSendMessage(sm))).mapTo[SendMessageAck] + + // TODO: figure out if we still need it. + // def writeMessage( + // peer: ApiPeer, + // senderUserId: Int, + // date: Instant, + // randomId: Long, + // message: ApiMessage + // ): Future[Unit] = + // withValidPeer(peer.asModel, senderUserId, failed = FastFuture.successful(())) { + // for { + // memberIds ← fetchMemberIds(DialogId(peer.asModel, senderUserId)) + // _ ← Future.sequence(memberIds map (writeMessageSelf(_, peer, senderUserId, new DateTime(date.toEpochMilli), randomId, message))) + // } yield () + // } def writeMessageSelf( userId: Int, peer: ApiPeer, senderUserId: Int, - date: DateTime, + dateMillis: Long, randomId: Long, message: ApiMessage ): Future[Unit] = @@ -182,7 +184,7 @@ final class DialogExtensionImpl(system: ActorSystem) extends DialogExtension wit envelope(Peer.privat(userId), DialogEnvelope().withWriteMessageSelf(WriteMessageSelf( dest = Some(peer.asModel), senderUserId, - date.getMillis, + dateMillis, randomId, message )))) map (_ ⇒ ()) @@ -216,7 +218,7 @@ final class DialogExtensionImpl(system: ActorSystem) extends DialogExtension wit // DialogRootOperations def unarchive(userId: Int, clientAuthId: Long, peer: Peer): Future[SeqState] = - withValidPeer(peer, userId, failed = Future.failed[SeqState](DialogErrors.MessageToSelf)) { + withValidPeer(peer, userId) { (userExt.processorRegion.ref ? UserEnvelope(userId) .withDialogRootEnvelope( @@ -226,7 +228,7 @@ final class DialogExtensionImpl(system: ActorSystem) extends DialogExtension wit } def archive(userId: Int, clientAuthId: Long, peer: Peer): Future[SeqState] = - withValidPeer(peer, userId, failed = Future.failed[SeqState](DialogErrors.MessageToSelf)) { + withValidPeer(peer, userId) { (userExt.processorRegion.ref ? UserEnvelope(userId) .withDialogRootEnvelope( @@ -236,7 +238,7 @@ final class DialogExtensionImpl(system: ActorSystem) extends DialogExtension wit } def favourite(userId: Int, clientAuthId: Long, peer: Peer): Future[SeqState] = - withValidPeer(peer, userId, failed = Future.failed[SeqState](DialogErrors.MessageToSelf)) { + withValidPeer(peer, userId) { (userExt.processorRegion.ref ? UserEnvelope(userId) .withDialogRootEnvelope( @@ -246,7 +248,7 @@ final class DialogExtensionImpl(system: ActorSystem) extends DialogExtension wit } def unfavourite(userId: Int, clientAuthId: Long, peer: Peer): Future[SeqState] = - withValidPeer(peer, userId, failed = Future.failed[SeqState](DialogErrors.MessageToSelf)) { + withValidPeer(peer, userId) { (userExt.processorRegion.ref ? UserEnvelope(userId) .withDialogRootEnvelope(DialogRootEnvelope().withUnfavourite(DialogRootCommands.Unfavourite(Some(peer), clientAuthId)))) @@ -260,6 +262,17 @@ final class DialogExtensionImpl(system: ActorSystem) extends DialogExtension wit .mapTo[SeqState] } + def bump(userId: Int, peer: Peer): Future[Unit] = + withValidPeer(peer, userId) { + (userExt.processorRegion.ref ? + UserEnvelope(userId) + .withDialogRootEnvelope( + DialogRootEnvelope().withBump(DialogRootCommands.Bump(Some(peer))) + )) + .mapTo[BumpAck] + .map(_ ⇒ ()) + } + def setReaction(userId: Int, authId: Long, peer: Peer, randomId: Long, code: String): Future[SetReactionAck] = withValidPeer(peer, userId) { (userExt.processorRegion.ref ? UserEnvelope(userId).withDialogEnvelope(DialogEnvelope().withSetReaction(SetReaction( @@ -292,8 +305,8 @@ final class DialogExtensionImpl(system: ActorSystem) extends DialogExtension wit extensions match { case Seq() ⇒ log.debug("No delivery extensions, using default one") - new ActorDelivery() - case ext +: tail ⇒ + new ActorDelivery(system) + case ext +: _ ⇒ log.debug("Got extensions: {}", extensions) val idToName = InternalExtensions.extensions(InternalDialogExtensions) idToName.get(ext.id) flatMap { className ⇒ @@ -378,21 +391,22 @@ final class DialogExtensionImpl(system: ActorSystem) extends DialogExtension wit .map(_.info.get) } + private val EmptyTextMessage = ApiTextMessage(text = "", mentions = Vector.empty, ext = None) + def getApiDialog(userId: Int, info: DialogInfo, sortDate: Instant): Future[ApiDialog] = { - val emptyMessageContent = ApiTextMessage(text = "", mentions = Vector.empty, ext = None) - val emptyMessage = HistoryMessage(userId, info.peer.get, new DateTime(0), 0, 0, emptyMessageContent.header, emptyMessageContent.toByteArray, None) + val emptyMessage = HistoryMessage(userId, info.peer.get, new DateTime(0), 0, 0, EmptyTextMessage.header, EmptyTextMessage.toByteArray, None) val peer = info.peer.get for { historyOwner ← getHistoryOwner(peer, userId) - messageOpt ← getLastMessage(userId, peer) - reactions ← messageOpt map (m ⇒ db.run(fetchReactions(peer, userId, m.randomId))) getOrElse FastFuture.successful(Vector.empty) - message ← getLastMessage(userId, peer) map (_ getOrElse emptyMessage) map (_.asStruct( + lastMessageOpt ← getLastMessage(historyOwner, peer) + reactions ← lastMessageOpt map (m ⇒ db.run(fetchReactions(peer, userId, m.randomId))) getOrElse FastFuture.successful(Vector.empty) + message = lastMessageOpt.getOrElse(emptyMessage).asStruct( lastReceivedAt = new DateTime(info.lastReceivedDate.toEpochMilli), lastReadAt = new DateTime(info.lastReadDate.toEpochMilli), reactions = reactions, attributes = None - )) map (_.getOrElse(throw new RuntimeException("Failed to get message struct"))) + ) getOrElse (throw new RuntimeException("Failed to get message struct")) firstUnreadOpt ← db.run(HistoryMessageRepo.findAfter(historyOwner, peer, new DateTime(info.lastReadDate.toEpochMilli), 1) map (_.headOption map (_.ofUser(userId)))) } yield ApiDialog( peer = peer.asStruct, @@ -423,8 +437,8 @@ final class DialogExtensionImpl(system: ActorSystem) extends DialogExtension wit .withDialogEnvelope(DialogEnvelope().withSendMessage(sendMessage))).mapTo[SeqStateDate] } - private def getLastMessage(userId: Int, peer: Peer): Future[Option[HistoryMessage]] = - db.run(HistoryMessageRepo.findNewest(userId, peer)) + private def getLastMessage(historyOwner: Int, peer: Peer): Future[Option[HistoryMessage]] = + db.run(HistoryMessageRepo.findNewest(historyOwner, peer)) private def reactions(events: Seq[ReactionEvent]): Seq[MessageReaction] = { (events.view groupBy (_.code) mapValues (_ map (_.userId)) map { @@ -462,22 +476,34 @@ final class DialogExtensionImpl(system: ActorSystem) extends DialogExtension wit } } +object DialogGroupKeys { + val Favourites = "favourites" + val Direct = "privates" + val Groups = "groups" +} + +object DialogGroupTitles { + val Favourites = "Favourites" + val Direct = "Direct Messages" + val Groups = "Groups" +} + object DialogExtension extends ExtensionId[DialogExtensionImpl] with ExtensionIdProvider { override def lookup = DialogExtension override def createExtension(system: ExtendedActorSystem) = new DialogExtensionImpl(system) def groupKey(group: DialogGroupType) = group match { - case DialogGroupType.Favourites ⇒ "favourites" - case DialogGroupType.DirectMessages ⇒ "privates" - case DialogGroupType.Groups ⇒ "groups" + case DialogGroupType.Favourites ⇒ DialogGroupKeys.Favourites + case DialogGroupType.DirectMessages ⇒ DialogGroupKeys.Direct + case DialogGroupType.Groups ⇒ DialogGroupKeys.Groups case unknown ⇒ throw DialogErrors.UnknownDialogGroupType(unknown) } def groupTitle(group: DialogGroupType) = group match { - case DialogGroupType.Favourites ⇒ "Favourites" - case DialogGroupType.DirectMessages ⇒ "Direct Messages" - case DialogGroupType.Groups ⇒ "Groups" + case DialogGroupType.Favourites ⇒ DialogGroupTitles.Favourites + case DialogGroupType.DirectMessages ⇒ DialogGroupTitles.Direct + case DialogGroupType.Groups ⇒ DialogGroupTitles.Groups case unknown ⇒ throw DialogErrors.UnknownDialogGroupType(unknown) } } diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogProcessor.scala b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogProcessor.scala index 19c4f30eb6..720885dfaa 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogProcessor.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogProcessor.scala @@ -1,8 +1,8 @@ package im.actor.server.dialog import akka.actor._ -import akka.event.Logging import akka.http.scaladsl.util.FastFuture +import akka.pattern.pipe import akka.util.Timeout import com.github.benmanes.caffeine.cache.Cache import im.actor.api.rpc.misc.ApiExtension @@ -115,6 +115,9 @@ private[dialog] final class DialogProcessor(val userId: Int, val peer: Peer, ext case WriteMessageSelf(_, senderUserId, date, randomId, message) ⇒ writeMessageSelf(senderUserId, date, randomId, message) } + // queries that dialog can actually reply when sending messages + def queries: Receive = handleQuery andThen (_ pipeTo sender()) + /** * dialog owner invokes `dc` * destination should be `peer` and origin should be `selfPeer` diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogProcessorMigration.scala b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogProcessorMigration.scala index d2a82d2678..98597997b4 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogProcessorMigration.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogProcessorMigration.scala @@ -5,6 +5,7 @@ import java.time.Instant import akka.actor.Status import akka.pattern.pipe import akka.persistence.SnapshotMetadata +import com.github.ghik.silencer.silent import im.actor.server.cqrs.{ Event, Processor } import im.actor.server.db.DbExtension import im.actor.server.model.{ DialogObsolete, Peer } @@ -80,7 +81,7 @@ trait DialogProcessorMigration extends Processor[DialogState] { private def migrate(): Unit = { log.warning("Starting migration") context become migrating - (db.run(DialogRepo.findDialog(userId, peer)) map { + (db.run(DialogRepo.findDialog(userId, peer): @silent) map { case Some(model) ⇒ model case _ ⇒ PersistEvents(List(Initialized())) }) pipeTo self diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogRoot.scala b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogRoot.scala index e65030de63..33a2c23a1d 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogRoot.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogRoot.scala @@ -107,6 +107,7 @@ private class DialogRoot(val userId: Int, extensions: Seq[ApiExtension]) import context.dispatcher private val system = context.system + private val seqUpdExt = SeqUpdatesExtension(system) private val userExt = UserExtension(system) private val groupExt = GroupExtension(system) private val db = DbExtension(system).db @@ -135,36 +136,13 @@ private class DialogRoot(val userId: Int, extensions: Seq[ApiExtension]) } override protected def handleCommand: Receive = { - case dc: DialogCommand if dc.isInstanceOf[SendMessage] || dc.isInstanceOf[WriteMessageSelf] ⇒ - needCheckDialog(dc) match { - case peerOpt @ Some(peer) ⇒ - val now = Instant.now - - val isCreated = isDialogCreated(peer) - val isShown = isDialogShown(peer) - - val events = if (isCreated) { - (if (!isShown) List(Unarchived(now, peerOpt)) else List.empty) ++ - (if (!isDialogOnTop(peer)) List(Bumped(now, peerOpt)) else List.empty) - } else { - log.debug("Creating dialog with peer type: {} id: {}", peerOpt.get.`type`, peerOpt.get.id) - List(Created(now, peerOpt)) - } - - persistAllAsync(events)(e ⇒ commit(e)) - - deferAsync(()) { _ ⇒ - if (!isCreated || !isShown) - sendChatGroupsChanged(0L) - } - case None ⇒ - } - case CheckArchive() ⇒ checkArchive() + case Bump(Some(peer)) ⇒ bump(peer) case Archive(Some(peer), clientAuthId) ⇒ archive(peer, clientAuthId) case Unarchive(Some(peer), clientAuthId) ⇒ unarchive(peer, clientAuthId) case Favourite(Some(peer), clientAuthId) ⇒ favourite(peer, clientAuthId) case Unfavourite(Some(peer), clientAuthId) ⇒ unfavourite(peer, clientAuthId) case Delete(Some(peer), clientAuthId) ⇒ delete(peer, clientAuthId) + case CheckArchive() ⇒ checkArchive() } private def checkArchive(): Unit = @@ -218,25 +196,33 @@ private class DialogRoot(val userId: Int, extensions: Seq[ApiExtension]) commit(e) (for { _ ← db.run(HistoryMessageRepo.deleteAll(userId, peer)) - _ ← SeqUpdatesExtension(system).deliverUserUpdate(userId, UpdateChatDelete(peer.asStruct)) - seqState ← sendChatGroupsChanged(clientAuthId) + seqState ← seqUpdExt.deliverClientUpdate(userId, clientAuthId, update = UpdateChatDelete(peer.asStruct)) + _ ← sendChatGroupsChanged(0L) // _ = thatDialog ! PoisonPill // kill that dialog would be good } yield seqState) pipeTo sender() } } - private def needCheckDialog(cmd: DialogCommand): Option[Peer] = { - cmd match { - case sm: SendMessage ⇒ - Some(sm.getOrigin.typ match { - case PeerType.Group ⇒ sm.getDest - case PeerType.Private ⇒ - if (selfPeer == sm.getDest) sm.getOrigin - else sm.getDest - case _ ⇒ throw new RuntimeException("Unknown peer type") - }) - case wm: WriteMessageSelf ⇒ Some(wm.getDest) - case _ ⇒ None + private def bump(peer: Peer) = { + val now = Instant.now + + val isCreated = isDialogCreated(peer) + val isShown = isDialogShown(peer) + + val createdEvt = if (!isCreated) Some(Created(now).withPeer(peer)) else None + val shownEvt = if (isCreated && !isShown) Some(Unarchived(now).withPeer(peer)) else None + val bumpedEvt = if (isCreated && !isDialogOnTop(peer)) Some(Bumped(now).withPeer(peer)) else None + + val events: List[DialogRootEvent] = (createdEvt ++ shownEvt ++ bumpedEvt).toList + + persistAllAsync(events)(e ⇒ commit(e)) + + val replyTo = sender() + deferAsync(()) { _ ⇒ + (if (!isCreated || !isShown) + sendChatGroupsChanged(0L) map (_ ⇒ BumpAck()) + else + FastFuture.successful(BumpAck())) pipeTo replyTo } } @@ -269,12 +255,15 @@ private class DialogRoot(val userId: Int, extensions: Seq[ApiExtension]) } private def sendChatGroupsChanged(authId: Long): Future[SeqState] = { - val pushRules = if (authId == 0L) PushRules() else PushRules().withExcludeAuthIds(Seq(authId)) for { groups ← DialogExtension(system).fetchApiGroupedDialogs(userId) update = UpdateChatGroupsChanged(groups) - seqState ← SeqUpdatesExtension(system) - .deliverClientUpdate(userId, authId, update, pushRules) + seqState ← seqUpdExt.deliverClientUpdate( + userId, + authId, + update, + reduceKey = Some(s"dialogschanged_${userId}") + ) } yield seqState } diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogRootMigration.scala b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogRootMigration.scala index 2705ba73ee..16ed9dced9 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogRootMigration.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogRootMigration.scala @@ -5,6 +5,7 @@ import java.time.Instant import akka.actor.Status import akka.pattern.pipe import akka.persistence.SnapshotMetadata +import com.github.ghik.silencer.silent import im.actor.server.cqrs.{ Event, Processor } import im.actor.server.db.DbExtension import im.actor.server.model.DialogObsolete @@ -58,7 +59,7 @@ trait DialogRootMigration extends Processor[DialogRootState] { context.become(migrating) (for { - models ← DbExtension(context.system).db.run(DialogRepo.fetchDialogs(userId)) + models ← DbExtension(context.system).db.run(DialogRepo.fetchDialogs(userId): @silent) } yield CreateEvents(models)) pipeTo self } diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogRootState.scala b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogRootState.scala index 57a223c8aa..32f292bae1 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogRootState.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/DialogRootState.scala @@ -263,4 +263,4 @@ private[dialog] final case class DialogRootState( copy(active = newActive) } -} \ No newline at end of file +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/HistoryUtils.scala b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/HistoryUtils.scala index 7a67ff99d9..13066aee40 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/HistoryUtils.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/HistoryUtils.scala @@ -21,7 +21,7 @@ object HistoryUtils { private[dialog] def writeHistoryMessage( fromPeer: Peer, toPeer: Peer, - date: DateTime, + dateMillis: Long, randomId: Long, messageContentHeader: Int, messageContentData: Array[Byte] @@ -30,6 +30,8 @@ object HistoryUtils { requirePrivatePeer(fromPeer) // requireDifferentPeers(fromPeer, toPeer) + val date = new DateTime(dateMillis) + if (toPeer.typ == PeerType.Private) { val outMessage = HistoryMessage( userId = fromPeer.id, @@ -79,7 +81,7 @@ object HistoryUtils { userId: Int, toPeer: Peer, senderUserId: Int, - date: DateTime, + dateMillis: Long, randomId: Long, messageContentHeader: Int, messageContentData: Array[Byte] @@ -88,7 +90,7 @@ object HistoryUtils { _ ← HistoryMessageRepo.create(HistoryMessage( userId = userId, peer = toPeer, - date = date, + date = new DateTime(dateMillis), senderUserId = senderUserId, randomId = randomId, messageContentHeader = messageContentHeader, @@ -98,23 +100,6 @@ object HistoryUtils { } yield () } - // todo: remove this in favor of getHistoryOwner - def withHistoryOwner[A](peer: Peer, clientUserId: Int)(f: Int ⇒ DBIO[A])(implicit system: ActorSystem): DBIO[A] = { - import system.dispatcher - (peer.typ match { - case PeerType.Private ⇒ DBIO.successful(clientUserId) - case PeerType.Group ⇒ - DBIO.from(GroupExtension(system).isHistoryShared(peer.id)) flatMap { isHistoryShared ⇒ - if (isHistoryShared) { - DBIO.successful(SharedUserId) - } else { - DBIO.successful(clientUserId) - } - } - case _ ⇒ throw new RuntimeException(s"Unknown peer type ${peer.typ}") - }) flatMap f - } - def getHistoryOwner(peer: Peer, clientUserId: Int)(implicit system: ActorSystem): Future[Int] = { import system.dispatcher peer.typ match { @@ -131,6 +116,6 @@ object HistoryUtils { private def requirePrivatePeer(peer: Peer) = { if (peer.typ != PeerType.Private) - throw new Exception("peer should be Private") + throw new RuntimeException("sender should be Private peer") } } diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/UserAcl.scala b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/UserAcl.scala index a41b746404..1127e2f0ac 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/dialog/UserAcl.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/dialog/UserAcl.scala @@ -11,6 +11,7 @@ trait UserAcl { protected val system: ActorSystem + // TODO: clarify names of params. protected def withNonBlockedPeer[A]( contactUserId: Int, peer: Peer @@ -23,16 +24,17 @@ trait UserAcl { } protected def withNonBlockedUser[A]( - contactUserId: Int, - contactOwnerUserId: Int + userId: Int, + ownerUserId: Int )(default: ⇒ Future[A], failed: ⇒ Future[A]): Future[A] = { import system.dispatcher for { - isBlocked ← checkIsBlocked(contactUserId, contactOwnerUserId) + isBlocked ← checkIsBlocked(userId, ownerUserId) result ← if (isBlocked) failed else default } yield result } - protected def checkIsBlocked(contactUserId: Int, contactOwnerUserId: Int): Future[Boolean] = - DbExtension(system).db.run(RelationRepo.isBlocked(contactOwnerUserId, contactUserId)) -} \ No newline at end of file + // check that `userId` is blocked by `ownerUserId` + protected def checkIsBlocked(userId: Int, ownerUserId: Int): Future[Boolean] = + DbExtension(system).db.run(RelationRepo.isBlocked(ownerUserId, userId)) +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/encryption/EncryptionApiConverters.scala b/actor-server/actor-core/src/main/scala/im/actor/server/encryption/EncryptionApiConverters.scala index 40ef212ffe..9bc89bd38b 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/encryption/EncryptionApiConverters.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/encryption/EncryptionApiConverters.scala @@ -2,7 +2,7 @@ package im.actor.server.encryption import cats.Foldable import cats.data.Xor, Xor._ -import cats.std.all._ +import cats.instances.all._ import cats.syntax.all._ import com.google.protobuf.ByteString import im.actor.api.rpc.encryption.{ ApiEncryptionKeySignature, ApiEncryptionKeyGroup, ApiEncryptionKey } diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/encryption/EncryptionExtension.scala b/actor-server/actor-core/src/main/scala/im/actor/server/encryption/EncryptionExtension.scala index 8393bd8db9..75b27dcc27 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/encryption/EncryptionExtension.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/encryption/EncryptionExtension.scala @@ -3,10 +3,9 @@ package im.actor.server.encryption import akka.actor._ import akka.event.Logging import akka.http.scaladsl.util.FastFuture -import cats.std.all._ +import cats.instances.all._ import cats.syntax.all._ import cats.data.{ Xor, XorT } -import com.google.protobuf.wrappers.Int32Value import im.actor.api.rpc.encryption._ import im.actor.cats.dbio._ import im.actor.server.db.DbExtension @@ -212,8 +211,8 @@ final class EncryptionExtension(system: ActorSystem) extends Extension { def checkBox( box: ApiEncryptedBox, ignoredKeyGroups: Map[Int, Set[Int]] - ): Future[Either[(Vector[ApiKeyGroupId], Vector[ApiKeyGroupId]), Map[Int, Vector[(Long, ApiEncryptedBox)]]]] = { - val userChecksFu: Iterable[Future[(Seq[ApiKeyGroupId], Seq[ApiKeyGroupId], Seq[EncryptionKeyGroup])]] = + ): Future[Either[(Vector[ApiKeyGroupHolder], Vector[ApiKeyGroupId]), Map[Int, Vector[(Long, ApiEncryptedBox)]]]] = { + val userChecksFu: Iterable[Future[(Seq[ApiKeyGroupHolder], Seq[ApiKeyGroupId], Seq[EncryptionKeyGroup])]] = box.keys.groupBy(_.usersId) map { case (userId, keys) ⇒ db.run(EncryptionKeyGroupRepo.fetch(userId)) map { kgs ⇒ @@ -223,7 +222,7 @@ final class EncryptionExtension(system: ActorSystem) extends Extension { val missingKgs = kgs.view .filterNot(kg ⇒ keys.exists(_.keyGroupId == kg.id)) .filterNot(kg ⇒ ignored.contains(kg.id)) - .map(kg ⇒ ApiKeyGroupId(userId, kg.id)) + .flatMap(kg ⇒ toApi(kg).toOption map (ApiKeyGroupHolder(userId, _))) .force // kgs presented in box but deleted by receiver @@ -237,7 +236,7 @@ final class EncryptionExtension(system: ActorSystem) extends Extension { Future.sequence(userChecksFu) map { checks ⇒ val (missing, obs, kgs) = - checks.foldLeft((Vector.empty[ApiKeyGroupId], Vector.empty[ApiKeyGroupId], Vector.empty[EncryptionKeyGroup])) { + checks.foldLeft((Vector.empty[ApiKeyGroupHolder], Vector.empty[ApiKeyGroupId], Vector.empty[EncryptionKeyGroup])) { case ((macc, oacc, kgacc), (m, o, kg)) ⇒ (macc ++ m, oacc ++ o, kgacc ++ kg) } diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/group/AdminCommandHandlers.scala b/actor-server/actor-core/src/main/scala/im/actor/server/group/AdminCommandHandlers.scala new file mode 100644 index 0000000000..cc568ef846 --- /dev/null +++ b/actor-server/actor-core/src/main/scala/im/actor/server/group/AdminCommandHandlers.scala @@ -0,0 +1,368 @@ +package im.actor.server.group + +import java.time.Instant + +import akka.actor.Status +import akka.pattern.pipe +import akka.http.scaladsl.util.FastFuture +import com.github.ghik.silencer.silent +import im.actor.api.rpc.Update +import im.actor.api.rpc.groups._ +import im.actor.api.rpc.messaging.UpdateChatClear +import im.actor.concurrent.FutureExt +import im.actor.server.CommonErrors +import im.actor.server.acl.ACLUtils +import im.actor.server.dialog.HistoryUtils +import im.actor.server.group.GroupCommands.{ DeleteGroup, DismissUserAdmin, MakeHistoryShared, MakeUserAdmin, RevokeIntegrationToken, RevokeIntegrationTokenAck, TransferOwnership, UpdateAdminSettings, UpdateAdminSettingsAck } +import im.actor.server.group.GroupErrors.{ NotAMember, NotAdmin, UserAlreadyAdmin, UserAlreadyNotAdmin } +import im.actor.server.group.GroupEvents.{ AdminSettingsUpdated, AdminStatusChanged, GroupDeleted, HistoryBecameShared, IntegrationTokenRevoked, OwnerChanged } +import im.actor.server.names.{ GlobalNameOwner, OwnerType } +import im.actor.server.persist.{ GroupBotRepo, GroupInviteTokenRepo, GroupUserRepo, HistoryMessageRepo } +import im.actor.server.sequence.{ SeqState, SeqStateDate } + +import scala.concurrent.Future + +private[group] trait AdminCommandHandlers extends GroupsImplicits { + this: GroupProcessor ⇒ + + protected def revokeIntegrationToken(cmd: RevokeIntegrationToken): Unit = { + if (!(state.isAdmin(cmd.clientUserId) || state.isOwner(cmd.clientUserId))) { + sender() ! notAdmin + } else { + val oldToken = state.bot.map(_.token) + val newToken = ACLUtils.accessToken() + + persist(IntegrationTokenRevoked(Instant.now, newToken)) { evt ⇒ + val newState = commit(evt) + + //TODO: remove deprecated + db.run(GroupBotRepo.updateToken(groupId, newToken): @silent) + + val result: Future[RevokeIntegrationTokenAck] = for { + _ ← oldToken match { + case Some(token) ⇒ integrationStorage.deleteToken(token) + case None ⇒ FastFuture.successful(()) + } + _ ← integrationStorage.upsertToken(newToken, groupId) + } yield RevokeIntegrationTokenAck(newToken) + + result pipeTo sender() + } + } + } + + // TODO: duplicate isBot check + protected def makeUserAdmin(cmd: MakeUserAdmin): Unit = { + if (!state.permissions.canEditAdmins(cmd.clientUserId)) { + sender() ! noPermission + } else if (state.nonMember(cmd.candidateUserId)) { + sender() ! Status.Failure(NotAMember) + } else if (state.isAdmin(cmd.candidateUserId)) { + sender() ! Status.Failure(UserAlreadyAdmin) + } else { + persist(AdminStatusChanged(Instant.now, cmd.candidateUserId, isAdmin = true)) { evt ⇒ + val newState = commit(evt) + + val dateMillis = evt.ts.toEpochMilli + val memberIds = newState.memberIds + val members = newState.members.values.map(_.asStruct).toVector + + val updateAdmin = UpdateGroupMemberAdminChanged(groupId, cmd.candidateUserId, isAdmin = true) + val updateMembers = UpdateGroupMembersUpdated(groupId, members) // don't push it! + // now this user is admin, change edit rules for admins + + val updatePermissions = permissionsUpdates(cmd.candidateUserId, newState) + // val updateCanEdit = UpdateGroupCanEditInfoChanged(groupId, canEditGroup = newState.adminSettings.canAdminsEditGroupInfo) + + val updateObsolete = UpdateGroupMembersUpdateObsolete(groupId, members) + + //TODO: remove deprecated + db.run(GroupUserRepo.makeAdmin(groupId, cmd.candidateUserId): @silent) + + val adminGROUPUpdates: Future[SeqStateDate] = + for { + _ ← seqUpdExt.broadcastPeopleUpdate( + userIds = memberIds + cmd.clientUserId, + updateAdmin + ) + SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( + cmd.clientUserId, + cmd.clientAuthId, + memberIds - cmd.clientUserId, + updateMembers + ) + } yield SeqStateDate(seq, state, dateMillis) + + val adminCHANNELUpdates: Future[SeqStateDate] = + for { + // push admin changed to all + _ ← seqUpdExt.broadcastPeopleUpdate( + userIds = memberIds + cmd.clientUserId, + updateAdmin + ) + // push changed members to admins and fresh admin + SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( + cmd.clientUserId, + cmd.clientAuthId, + (newState.adminIds - cmd.clientUserId) + cmd.candidateUserId, + updateMembers + ) + } yield SeqStateDate(seq, state, dateMillis) + + val result: Future[(Vector[ApiMember], SeqStateDate)] = for { + + /////////////////////////// + // Groups V1 API updates // + /////////////////////////// + + _ ← seqUpdExt.broadcastClientUpdate( + cmd.clientUserId, + cmd.clientAuthId, + memberIds - cmd.clientUserId, + updateObsolete + ) + + /////////////////////////// + // Groups V2 API updates // + /////////////////////////// + _ ← FutureExt.ftraverse(updatePermissions) { update ⇒ + seqUpdExt.deliverUserUpdate(cmd.candidateUserId, update) + } + seqStateDate ← if (state.groupType.isChannel) adminCHANNELUpdates else adminGROUPUpdates + + } yield (members, seqStateDate) + + result pipeTo sender() + } + } + } + + // TODO: duplicate isBot check + protected def dismissUserAdmin(cmd: DismissUserAdmin): Unit = { + if (!state.permissions.canEditAdmins(cmd.clientUserId)) { + sender() ! noPermission + } else if (state.nonMember(cmd.targetUserId)) { + sender() ! Status.Failure(NotAMember) + } else if (!state.isAdmin(cmd.targetUserId)) { + sender() ! Status.Failure(UserAlreadyNotAdmin) + } else { + persist(AdminStatusChanged(Instant.now, cmd.targetUserId, isAdmin = false)) { evt ⇒ + val newState = commit(evt) + + val memberIds = newState.memberIds + val members = newState.members.values.map(_.asStruct).toVector + + val updateAdmin = UpdateGroupMemberAdminChanged(groupId, cmd.targetUserId, isAdmin = false) + val updatePermissions = permissionsUpdates(cmd.targetUserId, newState) + + val updateObsolete = UpdateGroupMembersUpdateObsolete(groupId, members) + + //TODO: remove deprecated + db.run(GroupUserRepo.dismissAdmin(groupId, cmd.targetUserId): @silent) + + val result: Future[SeqState] = for { + + /////////////////////////// + // Groups V1 API updates // + /////////////////////////// + + _ ← seqUpdExt.broadcastClientUpdate( + cmd.clientUserId, + cmd.clientAuthId, + memberIds - cmd.clientUserId, + updateObsolete + ) + + /////////////////////////// + // Groups V2 API updates // + /////////////////////////// + _ ← FutureExt.ftraverse(updatePermissions) { update ⇒ + seqUpdExt.deliverUserUpdate(cmd.targetUserId, update) + } + seqState ← seqUpdExt.broadcastClientUpdate( + cmd.clientUserId, + cmd.clientAuthId, + memberIds - cmd.clientUserId, + updateAdmin + ) + } yield seqState + + result pipeTo sender() + } + } + } + + // TODO: duplicate isBot check + protected def transferOwnership(cmd: TransferOwnership): Unit = { + if (!state.isOwner(cmd.clientUserId)) { + sender() ! Status.Failure(CommonErrors.Forbidden) + } else { + persist(OwnerChanged(Instant.now, cmd.newOwnerId)) { evt ⇒ + val newState = commit(evt) + val memberIds = newState.memberIds + + val updatePermissionsPrevOwner = permissionsUpdates(cmd.clientUserId, newState) + val updatePermissionsNewOwner = permissionsUpdates(cmd.newOwnerId, newState) + + val result: Future[SeqState] = for { + // push permission updates to previous owner + _ ← FutureExt.ftraverse(updatePermissionsPrevOwner) { update ⇒ + seqUpdExt.deliverUserUpdate(cmd.clientUserId, update) + } + // push permission updates to new owner + _ ← FutureExt.ftraverse(updatePermissionsNewOwner) { update ⇒ + seqUpdExt.deliverUserUpdate(cmd.newOwnerId, update) + } + seqState ← seqUpdExt.broadcastClientUpdate( + userId = cmd.clientUserId, + authId = cmd.clientAuthId, + bcastUserIds = memberIds - cmd.clientUserId, + update = UpdateGroupOwnerChanged(groupId, cmd.newOwnerId), + pushRules = seqUpdExt.pushRules(isFat = false, None) + ) + } yield seqState + + result pipeTo sender() + } + } + } + + protected def updateAdminSettings(cmd: UpdateAdminSettings): Unit = { + if (!state.permissions.canEditAdminSettings(cmd.clientUserId)) { + sender() ! noPermission + } else if (AdminSettings.fromBitMask(cmd.settingsBitMask) == state.adminSettings) { + sender() ! UpdateAdminSettingsAck() + } else { + persist(AdminSettingsUpdated(Instant.now, cmd.settingsBitMask)) { evt ⇒ + val newState = commit(evt) + + //TODO: check if settings actually changed + val result: Future[UpdateAdminSettingsAck] = for { + // deliver permissions updates to all group members + // maybe there is no need to generate updates for each user + // three parts: members, admins, owner should be enough + _ ← FutureExt.ftraverse(newState.memberIds.toSeq) { userId ⇒ + FutureExt.ftraverse(permissionsUpdates(userId, newState)) { update ⇒ + seqUpdExt.deliverUserUpdate(userId, update) + } + } + } yield UpdateAdminSettingsAck() + + result pipeTo sender() + } + } + } + + protected def makeHistoryShared(cmd: MakeHistoryShared): Unit = { + if (!state.permissions.canMakeHistoryShared(cmd.clientUserId)) { + sender() ! noPermission + } else { + persist(HistoryBecameShared(Instant.now, cmd.clientUserId)) { evt ⇒ + val newState = commit(evt) + log.debug("History of group {} became shared", groupId) + + val memberIds = newState.memberIds + + val result: Future[SeqState] = for { + seqState ← seqUpdExt.broadcastClientUpdate( + userId = cmd.clientUserId, + authId = cmd.clientAuthId, + bcastUserIds = memberIds - cmd.clientUserId, + update = UpdateGroupHistoryShared(groupId) + ) + } yield seqState + + result pipeTo sender() + } + } + } + + protected def deleteGroup(cmd: DeleteGroup): Unit = { + if (!state.permissions.canDelete(cmd.clientUserId)) { + sender() ! noPermission + } else { + val exMemberIds = state.memberIds + val exGlobalName = state.shortName + val exGroupType = state.groupType + val exHistoryShared = state.isHistoryShared + val peer = apiGroupPeer.asModel + + persist(GroupDeleted(Instant.now, cmd.clientUserId)) { evt ⇒ + commit(evt) + + val ZeroPermissions = 0L + + val deleteGroupMembersUpdates: Vector[Update] = + Vector( + UpdateGroupMemberChanged(groupId, isMember = false), + // if channel, or group is big enough + (if (exGroupType.isChannel) + UpdateGroupMembersCountChanged(groupId, membersCount = 0) + else + UpdateGroupMembersUpdated(groupId, members = Vector.empty)), + UpdateGroupPermissionsChanged(groupId, ZeroPermissions), + UpdateGroupFullPermissionsChanged(groupId, ZeroPermissions), + UpdateGroupDeleted(groupId) + ) + + //TODO: remove deprecated. GroupInviteTokenRepo don't have replacement yet. + exMemberIds foreach { userId ⇒ + db.run( + for { + _ ← GroupUserRepo.delete(groupId, userId): @silent + _ ← GroupInviteTokenRepo.revoke(groupId, userId): @silent + } yield () + ) + } + + val result: Future[SeqState] = for { + // release global name of group + _ ← globalNamesStorage.updateOrRemove(exGlobalName, newGlobalName = None, GlobalNameOwner(OwnerType.Group, groupId)) + + // explicitly delete group history. + // TODO: move to utility method + _ ← if (exHistoryShared) { + db.run(HistoryMessageRepo.deleteAll(HistoryUtils.SharedUserId, peer)) + } else { + // for client user we delete history separately + FutureExt.ftraverse((exMemberIds - cmd.clientUserId).toSeq) { userId ⇒ + db.run(HistoryMessageRepo.deleteAll(userId, peer)) + } + } + + /////////////////////////// + // Groups V1 API updates // + /////////////////////////// + + // push all members updates about group members became empty + _ ← seqUpdExt.broadcastPeopleUpdate( + userIds = exMemberIds, + update = UpdateGroupMembersUpdateObsolete(groupId, members = Vector.empty) + ) + + /////////////////////////// + // Groups V2 API updates // + /////////////////////////// + + // send all members update about group became empty(no members) + _ ← Future.traverse(deleteGroupMembersUpdates) { update ⇒ + seqUpdExt.broadcastPeopleUpdate(exMemberIds, update) + } + + // send all members except clientUserId `UpdateChatClear` + _ ← seqUpdExt.broadcastPeopleUpdate( + userIds = exMemberIds - cmd.clientUserId, + update = UpdateChatClear(apiGroupPeer) + ) + + // delete dialog from client user's dialog list + // history deletion happens inside + seqState ← dialogExt.delete(cmd.clientUserId, cmd.clientAuthId, apiGroupPeer.asModel) + } yield seqState + + result pipeTo sender() + } + } + } + +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupCommandHandlers.scala b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupCommandHandlers.scala index d11c8c3269..dc3d8588fa 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupCommandHandlers.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupCommandHandlers.scala @@ -1,38 +1,38 @@ package im.actor.server.group -import java.time.{ Instant, LocalDateTime, ZoneOffset } +import java.time.Instant import akka.actor.Status import akka.http.scaladsl.util.FastFuture import akka.pattern.pipe +import com.github.ghik.silencer.silent import im.actor.api.rpc.Update -import im.actor.api.rpc.files.ApiAvatar import im.actor.api.rpc.groups._ import im.actor.api.rpc.users.ApiSex import im.actor.concurrent.FutureExt -import im.actor.server.CommonErrors import im.actor.server.acl.ACLUtils import im.actor.server.dialog.UserAcl -import im.actor.server.file.{ Avatar, ImageUtils } -import im.actor.server.group.GroupErrors._ -import im.actor.server.group.GroupEvents.{ AboutUpdated, AvatarUpdated, BotAdded, Created, IntegrationTokenRevoked, OwnerChanged, TitleUpdated, TopicUpdated, UserBecameAdmin, UserInvited, UserJoined, UserKicked, UserLeft } import im.actor.server.group.GroupCommands._ -import im.actor.server.model.{ AvatarData, Group } -import im.actor.server.office.PushTexts -import im.actor.server.persist.{ AvatarDataRepo, GroupBotRepo, GroupInviteTokenRepo, GroupRepo, GroupUserRepo } -import im.actor.server.sequence.{ Optimization, SeqState, SeqStateDate } +import im.actor.server.group.GroupErrors._ +import im.actor.server.group.GroupEvents._ +import im.actor.server.model.Group +import im.actor.server.persist.{ GroupBotRepo, GroupRepo, GroupUserRepo } +import im.actor.server.sequence.Optimization import im.actor.util.ThreadLocalSecureRandom import im.actor.util.misc.IdUtils import scala.concurrent.Future -private[group] trait GroupCommandHandlers extends GroupsImplicits with UserAcl { - self: GroupProcessor ⇒ - - import im.actor.server.ApiConversions._ +private[group] trait GroupCommandHandlers + extends MemberCommandHandlers + with InfoCommandHandlers + with AdminCommandHandlers + with UserAcl { + this: GroupProcessor ⇒ - private val notMember = Status.Failure(NotAMember) - private val notAdmin = Status.Failure(NotAdmin) + protected val notMember = Status.Failure(NotAMember) + protected val notAdmin = Status.Failure(NotAdmin) + protected val noPermission = Status.Failure(NoPermission) protected def create(cmd: Create): Unit = { if (!isValidTitle(cmd.title)) { @@ -61,16 +61,18 @@ private[group] trait GroupCommandHandlers extends GroupsImplicits with UserAcl { integrationStorage = new IntegrationTokensKeyValueStorage // Group creation + val groupType = GroupType.fromValue(cmd.typ) + persist(Created( ts = createdAt, groupId, - typ = Some(GroupType.fromValue(cmd.typ)), //FIXME: make it normal enum + typ = Some(groupType), creatorUserId = cmd.creatorUserId, accessHash = accessHash, title = cmd.title, userIds = Seq(cmd.creatorUserId), // only creator user becomes group member. all other users are invited via Invite message isHidden = Some(false), - isHistoryShared = Some(false), + isHistoryShared = Some(groupType.isChannel), extensions = Seq.empty )) { evt ⇒ val newState = commit(evt) @@ -95,21 +97,21 @@ private[group] trait GroupCommandHandlers extends GroupsImplicits with UserAcl { creatorUserId = newState.creatorUserId, accessHash = newState.accessHash, title = newState.title, - isPublic = newState.typ == GroupType.Public, + isPublic = false, createdAt = evt.ts, about = None, topic = None ), cmd.randomId, isHidden = false - ) - _ ← GroupUserRepo.create(groupId, cmd.creatorUserId, cmd.creatorUserId, createdAt, None, isAdmin = true) + ): @silent + _ ← GroupUserRepo.create(groupId, cmd.creatorUserId, cmd.creatorUserId, createdAt, None, isAdmin = true): @silent } yield () ) val result: Future[CreateAck] = for { /////////////////////////// - // old group api updates // + // Groups V1 API updates // /////////////////////////// _ ← seqUpdExt.deliverUserUpdate( @@ -121,7 +123,7 @@ private[group] trait GroupCommandHandlers extends GroupsImplicits with UserAcl { ) /////////////////////////// - // new group api updates // + // Groups V2 API updates // /////////////////////////// // send service message to group @@ -150,7 +152,7 @@ private[group] trait GroupCommandHandlers extends GroupsImplicits with UserAcl { val newState = commit(evt) //TODO: remove deprecated - db.run(GroupBotRepo.create(groupId, botUserId, botToken)) + db.run(GroupBotRepo.create(groupId, botUserId, botToken): @silent) (for { _ ← userExt.create(botUserId, ACLUtils.nextAccessSalt(), None, "Bot", "US", ApiSex.Unknown, isBot = true) @@ -163,779 +165,32 @@ private[group] trait GroupCommandHandlers extends GroupsImplicits with UserAcl { } } - protected def invite(cmd: Invite): Unit = { - if (state.isInvited(cmd.inviteeUserId)) { - sender() ! Status.Failure(GroupErrors.UserAlreadyInvited) - } else if (state.isMember(cmd.inviteeUserId)) { - sender() ! Status.Failure(GroupErrors.UserAlreadyJoined) - } else { - val inviteeIsExUser = state.isExUser(cmd.inviteeUserId) - - persist(UserInvited(Instant.now, cmd.inviteeUserId, cmd.inviterUserId)) { evt ⇒ - val newState = commit(evt) - - val dateMillis = evt.ts.toEpochMilli - val memberIds = newState.memberIds - val members = newState.members.values.map(_.asStruct).toVector - - // if user ever been in this group - we should push these updates, - // but don't push them if user is first time in group. in this case we should push FatSeqUpdate - val inviteeUpdatesNew: List[Update] = refreshGroupUpdates(newState) - - // send everyone in group, including invitee. - // send `FatSeqUpdate` if this user invited to group for first time. - val membersUpdateNew = UpdateGroupMembersUpdated(groupId, members) - - val inviteeUpdateObsolete = UpdateGroupInviteObsolete( - groupId, - inviteUserId = cmd.inviterUserId, - date = dateMillis, - randomId = cmd.randomId - ) - - val membersUpdateObsolete = UpdateGroupUserInvitedObsolete( - groupId, - userId = cmd.inviteeUserId, - inviterUserId = cmd.inviterUserId, - date = dateMillis, - randomId = cmd.randomId - ) - val serviceMessage = GroupServiceMessages.userInvited(cmd.inviteeUserId) - - //TODO: remove deprecated - db.run(GroupUserRepo.create(groupId, cmd.inviteeUserId, cmd.inviterUserId, evt.ts, None, isAdmin = false)) - - val result: Future[SeqStateDate] = for { - /////////////////////////// - // old group api updates // - /////////////////////////// - - // push "Invited" to invitee - _ ← seqUpdExt.deliverUserUpdate( - userId = cmd.inviteeUserId, - inviteeUpdateObsolete, - pushRules = seqUpdExt.pushRules(isFat = true, Some(PushTexts.Invited)), - deliveryId = s"invite_obsolete_${groupId}_${cmd.randomId}" - ) - - // push "User added" to all group members except for `inviterUserId` - _ ← seqUpdExt.broadcastPeopleUpdate( - (memberIds - cmd.inviteeUserId) - cmd.inviterUserId, // is it right? - membersUpdateObsolete, - pushRules = seqUpdExt.pushRules(isFat = true, Some(PushTexts.Added)), - deliveryId = s"useradded_obsolete_${groupId}_${cmd.randomId}" - ) - - // push "User added" to `inviterUserId` - _ ← seqUpdExt.deliverClientUpdate( - cmd.inviterUserId, - cmd.inviterAuthId, - membersUpdateObsolete, - pushRules = seqUpdExt.pushRules(isFat = true, None), - deliveryId = s"useradded_obsolete_${groupId}_${cmd.randomId}" - ) - - /////////////////////////// - // new group api updates // - /////////////////////////// - - // push updated members list to inviteeUserId, - _ ← seqUpdExt.deliverUserUpdate( - userId = cmd.inviteeUserId, - membersUpdateNew, - pushRules = seqUpdExt.pushRules(isFat = !inviteeIsExUser, Some(PushTexts.Invited)), - deliveryId = s"invite_${groupId}_${cmd.randomId}" - ) - - // push all group updates to inviteeUserId - _ ← FutureExt.ftraverse(inviteeUpdatesNew) { update ⇒ - seqUpdExt.deliverUserUpdate(userId = cmd.inviteeUserId, update) - } - - // push updated members list to all group members except inviteeUserId - SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( - userId = cmd.inviterUserId, - authId = cmd.inviterAuthId, - bcastUserIds = (memberIds - cmd.inviterUserId) - cmd.inviteeUserId, - update = membersUpdateNew, - deliveryId = s"useradded_${groupId}_${cmd.randomId}" - ) - - // explicitly send service message - SeqStateDate(_, _, date) ← dialogExt.sendServerMessage( - apiGroupPeer, - cmd.inviterUserId, - cmd.inviterAuthId, - cmd.randomId, - serviceMessage, - deliveryTag = Some(Optimization.GroupV2) - ) - } yield SeqStateDate(seq, state, date) - - result pipeTo sender() - } - } - } - - /** - * User can join - * • after invite(was invited by other user previously). In this case he already have group on devices - * • via invite ling. In this case he doesn't have group, and we need to deliver it. - */ - protected def join(cmd: Join): Unit = { - // user is already a member, and should not complete invitation process - if (state.isMember(cmd.joiningUserId) && !state.isInvited(cmd.joiningUserId)) { - sender() ! Status.Failure(GroupErrors.UserAlreadyJoined) - } else { - // user was invited in group by other group user - val wasInvited = state.isInvited(cmd.joiningUserId) - - val optMember = state.members.get(cmd.joiningUserId) - val inviterUserId = cmd.invitingUserId - .orElse(optMember.map(_.inviterUserId)) - .getOrElse(state.creatorUserId) - - persist(UserJoined(Instant.now, cmd.joiningUserId, inviterUserId)) { evt ⇒ - val newState = commit(evt) - - val date = evt.ts - val memberIds = newState.memberIds - val members = newState.members.values.map(_.asStruct).toVector - val randomId = ACLUtils.randomLong() - - // If user was never invited to group - he don't have group on devices, - // that means we need to push all group-info related updates - // - // If user was invited to group by other member - we don't need to push group updates, - // cause they we pushed already on invite step - val joiningUserUpdatesNew: List[Update] = - if (wasInvited) List.empty[Update] else refreshGroupUpdates(newState) - - // нужно отправить ему апдейт о членах в группе. - // всем нужно отправить апдейт о изменившихся членах в группе. можно в любом случае отправить - - // push to everyone, including joining user. - // if joining user wasn't invited - send update as FatSeqUpdate - // update date when member got into group - val membersUpdateNew = UpdateGroupMembersUpdated(groupId, members) - - val membersUpdateObsolete = UpdateGroupMembersUpdateObsolete(groupId, members) - - val serviceMessage = GroupServiceMessages.userJoined - - //TODO: remove deprecated - db.run(GroupUserRepo.create( - groupId, - userId = cmd.joiningUserId, - inviterUserId = inviterUserId, - invitedAt = optMember.map(_.invitedAt).getOrElse(date), - joinedAt = Some(LocalDateTime.now(ZoneOffset.UTC)), - isAdmin = false - )) - - val result: Future[(SeqStateDate, Vector[Int], Long)] = - for { - /////////////////////////// - // old group api updates // - /////////////////////////// - - // push update about members to all users, except joining user - _ ← seqUpdExt.broadcastPeopleUpdate( - memberIds - cmd.joiningUserId, - membersUpdateObsolete, - pushRules = seqUpdExt.pushRules(isFat = true, None), - deliveryId = s"userjoined_obsolete_${groupId}_${randomId}" - ) - - /////////////////////////// - // new group api updates // - /////////////////////////// - - // push all group updates to joiningUserId - _ ← FutureExt.ftraverse(joiningUserUpdatesNew) { update ⇒ - seqUpdExt.deliverUserUpdate(userId = cmd.joiningUserId, update) - } - - // push updated members list to joining user, - // TODO???: isFat = !wasInvited - is it correct? - SeqState(seq, state) ← seqUpdExt.deliverClientUpdate( - userId = cmd.joiningUserId, - authId = cmd.joiningUserAuthId, - update = membersUpdateNew, - pushRules = seqUpdExt.pushRules(isFat = !wasInvited, None), //!wasInvited means that user came for first time here - deliveryId = s"join_${groupId}_${randomId}" - - ) - - // push updated members list to all group members except joiningUserId - _ ← seqUpdExt.broadcastPeopleUpdate( - memberIds - cmd.joiningUserId, - membersUpdateNew, - deliveryId = s"userjoined_${groupId}_${randomId}" - ) - - SeqStateDate(_, _, date) ← dialogExt.sendServerMessage( - apiGroupPeer, - senderUserId = cmd.joiningUserId, - senderAuthId = cmd.joiningUserAuthId, - randomId = randomId, - serviceMessage // no delivery tag. This updated handled this way in Groups V1 - ) - } yield (SeqStateDate(seq, state, date), memberIds.toVector :+ inviterUserId, randomId) - - result pipeTo sender() - } - } - } - - /** - * This case handled in other manner, so we change state in the end - * cause user that left, should send service message. And we don't allow non-members - * to send message. So we keep him as member until message sent, and remove him from members - */ - protected def leave(cmd: Leave): Unit = { - if (state.nonMember(cmd.userId)) { - sender() ! notMember - } else { - persist(UserLeft(Instant.now, cmd.userId)) { evt ⇒ - // no commit here. it will be after service message sent - - val dateMillis = evt.ts.toEpochMilli - val members = state.members.filterNot(_._1 == cmd.userId).values.map(_.asStruct).toVector - - val updateObsolete = UpdateGroupUserLeaveObsolete(groupId, cmd.userId, dateMillis, cmd.randomId) - - val leftUserUpdatesNew = List( - UpdateGroupCanViewMembersChanged(groupId, canViewMembers = false), - UpdateGroupMembersUpdated(groupId, members = Vector.empty), - UpdateGroupCanInviteMembersChanged(groupId, canInviteMembers = false) - ) - - val membersUpdateNew = UpdateGroupMembersUpdated(groupId, members) - - val serviceMessage = GroupServiceMessages.userLeft - - //TODO: remove deprecated. GroupInviteTokenRepo don't have replacement yet. - db.run( - for { - _ ← GroupUserRepo.delete(groupId, cmd.userId) - _ ← GroupInviteTokenRepo.revoke(groupId, cmd.userId) - } yield () - ) - - // read this dialog by user that leaves group. don't wait for ack - dialogExt.messageRead(apiGroupPeer, cmd.userId, 0L, dateMillis) - val result: Future[SeqStateDate] = for { - - /////////////////////////// - // old group api updates // - /////////////////////////// - - _ ← seqUpdExt.broadcastClientUpdate( - userId = cmd.userId, - authId = cmd.authId, - bcastUserIds = state.memberIds + cmd.userId, // push this to other user's devices too. actually cmd.userId is still in state.memberIds - update = updateObsolete, - pushRules = seqUpdExt.pushRules(isFat = false, Some(PushTexts.Left), Seq(cmd.authId)) - ) - - /////////////////////////// - // new group api updates // - /////////////////////////// - - // push updated members list to all group members - _ ← seqUpdExt.broadcastPeopleUpdate( - state.memberIds - cmd.userId, - membersUpdateNew - ) - - SeqStateDate(_, _, date) ← dialogExt.sendServerMessage( - apiGroupPeer, - senderUserId = cmd.userId, - senderAuthId = cmd.authId, - randomId = cmd.randomId, - message = serviceMessage, - deliveryTag = Some(Optimization.GroupV2) - ) - - // push left user that he is no longer a member - SeqState(seq, state) ← seqUpdExt.deliverClientUpdate( - userId = cmd.userId, - authId = cmd.authId, - update = UpdateGroupMemberChanged(groupId, isMember = false) - ) - - // push left user updates - // • with empty group members - // • that he can't view and invite members - _ ← FutureExt.ftraverse(leftUserUpdatesNew) { update ⇒ - seqUpdExt.deliverUserUpdate(userId = cmd.userId, update) - } - - } yield SeqStateDate(seq, state, date) - - result andThen { case _ ⇒ commit(evt) } pipeTo sender() - } - } - } - - protected def kick(cmd: Kick): Unit = { - if (state.nonMember(cmd.kickerUserId) || state.nonMember(cmd.kickedUserId)) { - sender() ! notMember - } else { - persist(UserKicked(Instant.now, cmd.kickedUserId, cmd.kickerUserId)) { evt ⇒ - val newState = commit(evt) - - val dateMillis = evt.ts.toEpochMilli - val members = newState.members.values.map(_.asStruct).toVector - - val updateObsolete = UpdateGroupUserKickObsolete(groupId, cmd.kickedUserId, cmd.kickerUserId, dateMillis, cmd.randomId) - - val kickedUserUpdatesNew: List[Update] = List( - UpdateGroupCanViewMembersChanged(groupId, canViewMembers = false), - UpdateGroupMembersUpdated(groupId, members = Vector.empty), - UpdateGroupCanInviteMembersChanged(groupId, canInviteMembers = false), - UpdateGroupMemberChanged(groupId, isMember = false) - ) - - val membersUpdateNew = UpdateGroupMembersUpdated(groupId, members) - - val serviceMessage = GroupServiceMessages.userKicked(cmd.kickedUserId) - - //TODO: remove deprecated. GroupInviteTokenRepo don't have replacement yet. - db.run( - for { - _ ← GroupUserRepo.delete(groupId, cmd.kickedUserId) - _ ← GroupInviteTokenRepo.revoke(groupId, cmd.kickedUserId) - } yield () - ) - - // read this dialog by kicked user. don't wait for ack - dialogExt.messageRead(apiGroupPeer, cmd.kickedUserId, 0L, dateMillis) - val result: Future[SeqStateDate] = for { - /////////////////////////// - // old group api updates // - /////////////////////////// - - _ ← seqUpdExt.broadcastClientUpdate( - userId = cmd.kickerUserId, - authId = cmd.kickerAuthId, - bcastUserIds = newState.memberIds, - update = updateObsolete, - pushRules = seqUpdExt.pushRules(isFat = false, Some(PushTexts.Kicked), Seq(cmd.kickerAuthId)) - ) - - /////////////////////////// - // new group api updates // - /////////////////////////// - - // push updated members list to all group members. Don't push to kicked user! - SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( - userId = cmd.kickerUserId, - authId = cmd.kickerAuthId, - bcastUserIds = newState.memberIds - cmd.kickerUserId, - update = membersUpdateNew - ) - - SeqStateDate(_, _, date) ← dialogExt.sendServerMessage( - apiGroupPeer, - senderUserId = cmd.kickerUserId, - senderAuthId = cmd.kickerAuthId, - randomId = cmd.randomId, - message = serviceMessage, - deliveryTag = Some(Optimization.GroupV2) - ) - - // push kicked user updates - // • with empty group members - // • that he is no longer a member of group - // • that he can't view and invite members - _ ← FutureExt.ftraverse(kickedUserUpdatesNew) { update ⇒ - seqUpdExt.deliverUserUpdate(userId = cmd.kickedUserId, update) - } - - } yield SeqStateDate(seq, state, date) - - result pipeTo sender() - } - } - } - - protected def updateAvatar(cmd: UpdateAvatar): Unit = { - if (state.nonMember(cmd.clientUserId)) { - sender() ! notMember - } else { - persist(AvatarUpdated(Instant.now, cmd.avatar)) { evt ⇒ - val newState = commit(evt) - - val dateMillis = evt.ts.toEpochMilli - val apiAvatar: Option[ApiAvatar] = cmd.avatar - val memberIds = newState.memberIds - - val updateNew = UpdateGroupAvatarChanged(groupId, apiAvatar) - val updateObsolete = UpdateGroupAvatarChangedObsolete(groupId, cmd.clientUserId, apiAvatar, dateMillis, cmd.randomId) - val serviceMessage = GroupServiceMessages.changedAvatar(apiAvatar) - - db.run(AvatarDataRepo.createOrUpdate(getAvatarData(cmd.avatar))) - val result: Future[UpdateAvatarAck] = for { - /////////////////////////// - // old group api updates // - /////////////////////////// - - _ ← seqUpdExt.broadcastClientUpdate(cmd.clientUserId, cmd.clientAuthId, memberIds, updateObsolete) - - /////////////////////////// - // new group api updates // - /////////////////////////// - - SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( - userId = cmd.clientUserId, - authId = cmd.clientAuthId, - bcastUserIds = memberIds - cmd.clientUserId, - update = updateNew - ) - SeqStateDate(_, _, date) ← dialogExt.sendServerMessage( - apiGroupPeer, - senderUserId = cmd.clientUserId, - senderAuthId = cmd.clientAuthId, - randomId = cmd.randomId, - message = serviceMessage, - deliveryTag = Some(Optimization.GroupV2) - ) - } yield UpdateAvatarAck(apiAvatar).withSeqStateDate(SeqStateDate(seq, state, date)) - - result pipeTo sender() - } - } - } - - protected def updateTitle(cmd: UpdateTitle): Unit = { - val title = cmd.title - if (state.nonMember(cmd.clientUserId)) { - sender() ! notMember - } else if (!isValidTitle(title)) { - sender() ! Status.Failure(InvalidTitle) - } else { - persist(TitleUpdated(Instant.now(), title)) { evt ⇒ - val newState = commit(evt) - - val dateMillis = evt.ts.toEpochMilli - val memberIds = newState.memberIds - - val updateNew = UpdateGroupTitleChanged(groupId, title) - val updateObsolete = UpdateGroupTitleChangedObsolete( - groupId, - userId = cmd.clientUserId, - title = title, - date = dateMillis, - randomId = cmd.randomId - ) - val serviceMessage = GroupServiceMessages.changedTitle(title) - val pushRules = seqUpdExt.pushRules(isFat = false, Some(PushTexts.TitleChanged)) - - //TODO: remove deprecated - db.run(GroupRepo.updateTitle(groupId, title, cmd.clientUserId, cmd.randomId, date = evt.ts)) - - val result: Future[SeqStateDate] = for { - - /////////////////////////// - // old group api updates // - /////////////////////////// - - _ ← seqUpdExt.broadcastClientUpdate( - cmd.clientUserId, - cmd.clientAuthId, - memberIds - cmd.clientUserId, - updateObsolete, - pushRules - ) - - /////////////////////////// - // new group api updates // - /////////////////////////// - - SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( - userId = cmd.clientUserId, - authId = cmd.clientAuthId, - bcastUserIds = memberIds - cmd.clientUserId, - update = updateNew, - pushRules = pushRules - ) - SeqStateDate(_, _, date) ← dialogExt.sendServerMessage( - apiGroupPeer, - senderUserId = cmd.clientUserId, - senderAuthId = cmd.clientAuthId, - randomId = cmd.randomId, - message = serviceMessage, - deliveryTag = Some(Optimization.GroupV2) - ) - } yield SeqStateDate(seq, state, date) - - result pipeTo sender() - } - - } - } - - protected def updateTopic(cmd: UpdateTopic): Unit = { - def isValidTopic(topic: Option[String]) = topic.forall(t ⇒ t.nonEmpty && t.length < 255) - - val topic = cmd.topic map (_.trim) - - if (state.nonMember(cmd.clientUserId)) { - sender() ! notMember - } else if (!isValidTopic(topic)) { - sender() ! Status.Failure(TopicTooLong) - } else { - persist(TopicUpdated(Instant.now, topic)) { evt ⇒ - val newState = commit(evt) - - val dateMillis = evt.ts.toEpochMilli - val memberIds = newState.memberIds - - val updateNew = UpdateGroupTopicChanged(groupId, topic) - val updateObsolete = UpdateGroupTopicChangedObsolete( - groupId, - randomId = cmd.randomId, - userId = cmd.clientUserId, - topic = topic, - date = dateMillis - ) - val serviceMessage = GroupServiceMessages.changedTopic(topic) - val pushRules = seqUpdExt.pushRules(isFat = false, Some(PushTexts.TopicChanged)) - - //TODO: remove deprecated - db.run(GroupRepo.updateTopic(groupId, topic)) - - val result: Future[SeqStateDate] = for { - - /////////////////////////// - // old group api updates // - /////////////////////////// - - _ ← seqUpdExt.broadcastClientUpdate( - cmd.clientUserId, - cmd.clientAuthId, - memberIds - cmd.clientUserId, - updateObsolete, - pushRules - ) - - /////////////////////////// - // new group api updates // - /////////////////////////// - - SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( - userId = cmd.clientUserId, - authId = cmd.clientAuthId, - bcastUserIds = memberIds - cmd.clientUserId, - update = updateNew, - pushRules = pushRules - ) - SeqStateDate(_, _, date) ← dialogExt.sendServerMessage( - apiGroupPeer, - senderUserId = cmd.clientUserId, - senderAuthId = cmd.clientAuthId, - randomId = cmd.randomId, - message = serviceMessage, - deliveryTag = Some(Optimization.GroupV2) - ) - } yield SeqStateDate(seq, state, date) - - result pipeTo sender() - } - } - } - - protected def updateAbout(cmd: UpdateAbout): Unit = { - def isValidAbout(about: Option[String]) = about.forall(a ⇒ a.nonEmpty && a.length < 255) - - val about = cmd.about map (_.trim) + protected def permissionsUpdates(userId: Int, currState: GroupState): Vector[Update] = + Vector( + UpdateGroupPermissionsChanged(groupId, currState.permissions.groupFor(userId)), + UpdateGroupFullPermissionsChanged(groupId, currState.permissions.fullFor(userId)) + ) - if (!state.isAdmin(cmd.clientUserId)) { - sender() ! notAdmin - } else if (!isValidAbout(about)) { - sender() ! Status.Failure(AboutTooLong) - } else { - - persist(AboutUpdated(Instant.now, about)) { evt ⇒ - val newState = commit(evt) - - val memberIds = newState.memberIds - - val updateNew = UpdateGroupAboutChanged(groupId, about) - val updateObsolete = UpdateGroupAboutChangedObsolete(groupId, about) - val serviceMessage = GroupServiceMessages.changedAbout(about) - val pushRules = seqUpdExt.pushRules(isFat = false, Some(PushTexts.TopicChanged)) + protected def isValidTitle(title: String) = title.nonEmpty && title.length < 255 - //TODO: remove deprecated - db.run(GroupRepo.updateAbout(groupId, about)) - - val result: Future[SeqStateDate] = for { - - /////////////////////////// - // old group api updates // - /////////////////////////// - - _ ← seqUpdExt.broadcastClientUpdate( - cmd.clientUserId, - cmd.clientAuthId, - memberIds - cmd.clientUserId, - updateObsolete, - pushRules - ) - - /////////////////////////// - // new group api updates // - /////////////////////////// - - SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( - userId = cmd.clientUserId, - authId = cmd.clientAuthId, - bcastUserIds = memberIds - cmd.clientUserId, - update = updateNew, - pushRules = pushRules - ) - SeqStateDate(_, _, date) ← dialogExt.sendServerMessage( - apiGroupPeer, - senderUserId = cmd.clientUserId, - senderAuthId = cmd.clientAuthId, - randomId = cmd.randomId, - message = serviceMessage, - deliveryTag = Some(Optimization.GroupV2) - ) - } yield SeqStateDate(seq, state, date) - - result pipeTo sender() + protected def updateCanCall(currState: GroupState): Unit = { + log.debug(s"Group {} can call updated", groupId) + currState.memberIds foreach { userId ⇒ + permissionsUpdates(userId, currState) foreach { update ⇒ + seqUpdExt.deliverUserUpdate(userId, update) } } } - protected def revokeIntegrationToken(cmd: RevokeIntegrationToken): Unit = { - if (!state.isAdmin(cmd.clientUserId)) { - sender() ! notAdmin - } else { - val oldToken = state.bot.map(_.token) - val newToken = ACLUtils.accessToken() - - persist(IntegrationTokenRevoked(Instant.now, newToken)) { evt ⇒ - val newState = commit(evt) - - //TODO: remove deprecated - db.run(GroupBotRepo.updateToken(groupId, newToken)) - - val result: Future[RevokeIntegrationTokenAck] = for { - _ ← oldToken match { - case Some(token) ⇒ integrationStorage.deleteToken(token) - case None ⇒ FastFuture.successful(()) - } - _ ← integrationStorage.upsertToken(newToken, groupId) - } yield RevokeIntegrationTokenAck(newToken) + protected def makeMembersAsync(): Unit = { + persist(MembersBecameAsync(Instant.now)) { evt ⇒ + val newState = commit(evt) + log.debug(s"Group {} became async members", groupId) - result pipeTo sender() - } - } - } - - protected def makeUserAdmin(cmd: MakeUserAdmin): Unit = { - if (!state.isAdmin(cmd.clientUserId) || state.nonMember(cmd.candidateUserId)) { - sender() ! Status.Failure(NotAdmin) - } else if (state.isAdmin(cmd.candidateUserId)) { - sender() ! Status.Failure(UserAlreadyAdmin) - } else { - persist(UserBecameAdmin(Instant.now, cmd.candidateUserId, cmd.clientUserId)) { evt ⇒ - val newState = commit(evt) - - val dateMillis = evt.ts.toEpochMilli - val memberIds = newState.memberIds - val members = newState.members.values.map(_.asStruct).toVector - - val updateAdmin = UpdateGroupMemberAdminChanged(groupId, cmd.candidateUserId, isAdmin = true) - val updateMembers = UpdateGroupMembersUpdated(groupId, members) - - val updateObsolete = UpdateGroupMembersUpdateObsolete(groupId, members) - - //TODO: remove deprecated - db.run(GroupUserRepo.makeAdmin(groupId, cmd.candidateUserId)) - - val result: Future[(Vector[ApiMember], SeqStateDate)] = for { - - /////////////////////////// - // old group api updates // - /////////////////////////// - - _ ← seqUpdExt.broadcastClientUpdate( - cmd.clientUserId, - cmd.clientAuthId, - memberIds - cmd.clientUserId, - updateObsolete - ) - - /////////////////////////// - // new group api updates // - /////////////////////////// - - _ ← seqUpdExt.broadcastPeopleUpdate( - userIds = memberIds + cmd.clientUserId, - updateAdmin - ) - SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( - cmd.clientUserId, - cmd.clientAuthId, - memberIds - cmd.clientUserId, - updateMembers - ) - } yield (members, SeqStateDate(seq, state, dateMillis)) - - result pipeTo sender() - } + seqUpdExt.broadcastPeopleUpdate( + userIds = newState.memberIds, + update = UpdateGroupMembersBecameAsync(groupId) + ) } } - - protected def transferOwnership(cmd: TransferOwnership): Unit = { - if (!state.isOwner(cmd.clientUserId)) { - sender() ! Status.Failure(CommonErrors.Forbidden) - } else { - persist(OwnerChanged(Instant.now, cmd.newOwnerId)) { evt ⇒ - val newState = commit(evt) - val memberIds = newState.memberIds - - val result: Future[SeqState] = for { - seqState ← seqUpdExt.broadcastClientUpdate( - userId = cmd.clientUserId, - authId = cmd.clientAuthId, - bcastUserIds = memberIds - cmd.clientUserId, - update = UpdateGroupOwnerChanged(groupId, cmd.newOwnerId), - pushRules = seqUpdExt.pushRules(isFat = false, None) - ) - } yield seqState - - result pipeTo sender() - } - } - } - - private def getAvatarData(avatar: Option[Avatar]): AvatarData = - avatar - .map(ImageUtils.getAvatarData(AvatarData.OfGroup, groupId, _)) - .getOrElse(AvatarData.empty(AvatarData.OfGroup, groupId.toLong)) - - private def isValidTitle(title: String) = title.nonEmpty && title.length < 255 - - // Updates that will be sent to user, when he enters group. - // Helps clients that have this group to refresh it's data. - // TODO: review when chanels will be added - private def refreshGroupUpdates(newState: GroupState): List[Update] = List( - UpdateGroupMemberChanged(groupId, isMember = true), - UpdateGroupAboutChanged(groupId, newState.about), - UpdateGroupAvatarChanged(groupId, newState.avatar), - UpdateGroupTopicChanged(groupId, newState.topic), - UpdateGroupTitleChanged(groupId, newState.title), - UpdateGroupOwnerChanged(groupId, newState.ownerUserId), - UpdateGroupCanViewMembersChanged(groupId, canViewMembers = true), - UpdateGroupCanInviteMembersChanged(groupId, canInviteMembers = true) - // UpdateGroupExtChanged(groupId, newState.extension) //TODO: figure out and fix - // if(bigGroup) UpdateGroupMembersCountChanged(groupId, newState.extension) - ) - } diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupErrors.scala b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupErrors.scala index 4ac0ad30c5..1decbe7c08 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupErrors.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupErrors.scala @@ -7,6 +7,8 @@ import scala.util.control.NoStackTrace object GroupErrors { final case class GroupNotFound(id: Int) extends EntityNotFound(s"Group $id not found") + final case class GroupAlreadyDeleted(id: Int) extends EntityNotFound(s"Group $id deleted") + final case class GroupIdAlreadyExists(id: Int) extends Exception with NoStackTrace object NotAMember extends Exception("Not a group member") with NoStackTrace @@ -17,15 +19,33 @@ object GroupErrors { case object UserAlreadyAdmin extends Exception with NoStackTrace + case object UserAlreadyNotAdmin extends Exception with NoStackTrace + case object NotAdmin extends Exception with NoStackTrace + case object NotOwner extends Exception with NoStackTrace + case object InvalidTitle extends Exception with NoStackTrace case object AboutTooLong extends Exception with NoStackTrace + case object InvalidShortName extends Exception with NoStackTrace + + case object ShortNameTaken extends Exception with NoStackTrace + case object TopicTooLong extends Exception with NoStackTrace case object NoBotFound extends Exception with NoStackTrace case object BlockedByUser extends Exception with NoStackTrace + + case object UserIsBanned extends Exception with NoStackTrace + + case object NoPermission extends Exception with NoStackTrace + + case object CantLeaveGroup extends Exception with NoStackTrace + + final case class IncorrectGroupType(value: Int) extends Exception with NoStackTrace + + case object InvalidExtension extends Exception with NoStackTrace } diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupMigrator.scala b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupMigrator.scala index ab83851a7a..4ccea6d4f4 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupMigrator.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupMigrator.scala @@ -9,6 +9,7 @@ import scala.concurrent.{ ExecutionContext, Future, Promise } import akka.actor.{ ActorSystem, Props } import akka.pattern.pipe import akka.persistence.RecoveryCompleted +import com.github.ghik.silencer.silent import im.actor.server.db.DbExtension import org.joda.time.DateTime import im.actor.server.event.TSEvent @@ -49,12 +50,12 @@ private final class GroupMigrator(promise: Promise[Unit], groupId: Int) extends override def persistenceId = GroupProcessor.persistenceIdFor(groupId) private def migrate(): Unit = { - db.run(GroupRepo.findFull(groupId)) foreach { + db.run(GroupRepo.findFull(groupId): @silent) foreach { case Some(group) ⇒ db.run(for { avatarOpt ← AvatarDataRepo.findByGroupId(groupId) - bots ← GroupBotRepo.findByGroup(groupId) map (_.map(Seq(_)).getOrElse(Seq.empty)) - users ← GroupUserRepo.find(groupId) + bots ← (GroupBotRepo.findByGroup(groupId): @silent) map (_.map(Seq(_)).getOrElse(Seq.empty)) + users ← GroupUserRepo.find(groupId): @silent } yield Migrate( group = group, avatarData = avatarOpt, diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupOperations.scala b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupOperations.scala index d350a2657d..8a9724716d 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupOperations.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupOperations.scala @@ -5,7 +5,7 @@ import akka.pattern.ask import akka.util.Timeout import com.google.protobuf.ByteString import im.actor.api.rpc.AuthorizedClientData -import im.actor.api.rpc.groups.{ ApiGroup, ApiGroupFull, ApiMember } +import im.actor.api.rpc.groups.{ ApiAdminSettings, ApiGroup, ApiGroupFull, ApiMember } import im.actor.server.dialog.UserAcl import im.actor.server.file.Avatar import im.actor.server.sequence.{ SeqState, SeqStateDate } @@ -46,8 +46,7 @@ private[group] sealed trait Commands extends UserAcl { def joinGroup(groupId: Int, joiningUserId: Int, joiningUserAuthId: Long, invitingUserId: Option[Int]): Future[(SeqStateDate, Vector[Int], Long)] = (processorRegion.ref ? GroupEnvelope(groupId) - .withJoin(Join(joiningUserId, joiningUserAuthId, invitingUserId = None)) //None? - ).mapTo[(SeqStateDate, Vector[Int], Long)] + .withJoin(Join(joiningUserId, joiningUserAuthId, invitingUserId = invitingUserId))).mapTo[(SeqStateDate, Vector[Int], Long)] def inviteToGroup(groupId: Int, inviteeUserId: Int, randomId: Long)(implicit client: AuthorizedClientData): Future[SeqStateDate] = inviteToGroup(client.userId, client.authId, groupId, inviteeUserId, randomId) @@ -88,11 +87,21 @@ private[group] sealed trait Commands extends UserAcl { GroupEnvelope(groupId) .withUpdateAbout(UpdateAbout(clientUserId, clientAuthId, about, randomId))).mapTo[SeqStateDate] + def updateShortName(groupId: Int, clientUserId: Int, clientAuthId: Long, shortName: Option[String]): Future[SeqState] = + (processorRegion.ref ? + GroupEnvelope(groupId) + .withUpdateShortName(UpdateShortName(clientUserId, clientAuthId, shortName))).mapTo[SeqState] + def makeUserAdmin(groupId: Int, clientUserId: Int, clientAuthId: Long, candidateId: Int): Future[(Vector[ApiMember], SeqStateDate)] = (processorRegion.ref ? GroupEnvelope(groupId) .withMakeUserAdmin(MakeUserAdmin(clientUserId, clientAuthId, candidateId))).mapTo[(Vector[ApiMember], SeqStateDate)] + def dismissUserAdmin(groupId: Int, clientUserId: Int, clientAuthId: Long, targetUserId: Int): Future[SeqState] = + (processorRegion.ref ? + GroupEnvelope(groupId) + .withDismissUserAdmin(DismissUserAdmin(clientUserId, clientAuthId, targetUserId))).mapTo[SeqState] + def revokeIntegrationToken(groupId: Int, clientUserId: Int): Future[String] = (processorRegion.ref ? GroupEnvelope(groupId) @@ -103,6 +112,31 @@ private[group] sealed trait Commands extends UserAcl { GroupEnvelope(groupId) .withTransferOwnership(TransferOwnership(clientUserId, clientAuthId, newOwnerId))).mapTo[SeqState] + def updateAdminSettings(groupId: Int, clientUserId: Int, settings: ApiAdminSettings): Future[Unit] = + (processorRegion.ref ? + GroupEnvelope(groupId) + .withUpdateAdminSettings(UpdateAdminSettings(clientUserId, AdminSettings.apiToBitMask(settings)))).mapTo[UpdateAdminSettingsAck] map (_ ⇒ ()) + + def makeHistoryShared(groupId: Int, clientUserId: Int, clientAuthId: Long): Future[SeqState] = + (processorRegion.ref ? + GroupEnvelope(groupId) + .withMakeHistoryShared(MakeHistoryShared(clientUserId, clientAuthId))).mapTo[SeqState] + + def deleteGroup(groupId: Int, clientUserId: Int, clientAuthId: Long): Future[SeqState] = + (processorRegion.ref ? + GroupEnvelope(groupId) + .withDeleteGroup(DeleteGroup(clientUserId, clientAuthId))).mapTo[SeqState] + + def addExt(groupId: Int, ext: GroupExt): Future[Unit] = + (processorRegion.ref ? + GroupEnvelope(groupId) + .withAddExt(AddExt(Some(ext)))).mapTo[AddExtAck] map (_ ⇒ ()) + + def removeExt(groupId: Int, key: String): Future[Unit] = + (processorRegion.ref ? + GroupEnvelope(groupId) + .withRemoveExt(RemoveExt(key))).mapTo[RemoveExtAck] map (_ ⇒ ()) + } private[group] sealed trait Queries { @@ -124,20 +158,20 @@ private[group] sealed trait Queries { GroupEnvelope(groupId) .withGetIntegrationToken(GetIntegrationToken(clientUserId = None))).mapTo[GetIntegrationTokenResponse] map (_.token) - def getApiStruct(groupId: Int, clientUserId: Int): Future[ApiGroup] = + def getApiStruct(groupId: Int, clientUserId: Int, loadGroupMembers: Boolean = true): Future[ApiGroup] = (viewRegion.ref ? GroupEnvelope(groupId) - .withGetApiStruct(GetApiStruct(clientUserId))).mapTo[GetApiStructResponse] map (_.struct) + .withGetApiStruct(GetApiStruct(clientUserId, loadGroupMembers))).mapTo[GetApiStructResponse] map (_.struct) def getApiFullStruct(groupId: Int, clientUserId: Int): Future[ApiGroupFull] = (viewRegion.ref ? GroupEnvelope(groupId) .withGetApiFullStruct(GetApiFullStruct(clientUserId))).mapTo[GetApiFullStructResponse] map (_.struct) - def isPublic(groupId: Int): Future[Boolean] = + def isChannel(groupId: Int): Future[Boolean] = (viewRegion.ref ? GroupEnvelope(groupId) - .withIsPublic(IsPublic())).mapTo[IsPublicResponse] map (_.isPublic) + .withIsChannel(IsChannel())).mapTo[IsChannelResponse] map (_.isChannel) def isHistoryShared(groupId: Int): Future[Boolean] = (viewRegion.ref ? @@ -149,12 +183,25 @@ private[group] sealed trait Queries { GroupEnvelope(groupId) .withCheckAccessHash(CheckAccessHash(hash))).mapTo[CheckAccessHashResponse] map (_.isCorrect) + // TODO: should be signed as internal API, and become narrowly scoped + // never use it in for client queries //(memberIds, invitedUserIds, botId) - def getMemberIds(groupId: Int): Future[(Seq[Int], Seq[Int], Option[Int])] = + def getMemberIds(groupId: Int): Future[(Seq[Int], Seq[Int], Option[Int])] = //TODO: prepare for channel (viewRegion.ref ? GroupEnvelope(groupId) .withGetMembers(GetMembers())).mapTo[GetMembersResponse] map (r ⇒ (r.memberIds, r.invitedUserIds, r.botId)) + // TODO: should be signed as internal API + // TODO: better name maybe + def canSendMessage(groupId: Int, clientUserId: Int): Future[CanSendMessageInfo] = + (viewRegion.ref ? + GroupEnvelope(groupId) + .withCanSendMessage(CanSendMessage(clientUserId))).mapTo[CanSendMessageResponse] map { + case CanSendMessageResponse(canSend, isChannel, memberIds, botId) ⇒ + CanSendMessageInfo(canSend, isChannel, memberIds.toSet, botId) + } + + //TODO: move to separate Query. def isMember(groupId: Int, userId: Int): Future[Boolean] = getMemberIds(groupId) map (_._1.contains(userId)) @@ -171,5 +218,25 @@ private[group] sealed trait Queries { def loadMembers(groupId: Int, clientUserId: Int, limit: Int, offset: Option[Array[Byte]]) = (viewRegion.ref ? GroupEnvelope(groupId) - .withLoadMembers(LoadMembers(clientUserId, limit, offset map ByteString.copyFrom))).mapTo[LoadMembersResponse] map (r ⇒ r.userIds → r.offset.map(_.toByteArray)) + .withLoadMembers(LoadMembers(clientUserId, limit, offset map ByteString.copyFrom))).mapTo[LoadMembersResponse] map { resp ⇒ + ( + resp.members map { m ⇒ + ApiMember(m.userId, m.inviterUserId, m.invitedAt, isAdmin = Some(m.isAdmin)) + }, + resp.offset.map(_.toByteArray) + ) + } + + def loadAdminSettings(groupId: Int, clientUserId: Int): Future[ApiAdminSettings] = { + (viewRegion.ref ? + GroupEnvelope(groupId) + .withLoadAdminSettings(LoadAdminSettings(clientUserId))).mapTo[LoadAdminSettingsResponse] map (_.settings) + } } + +final case class CanSendMessageInfo( + canSend: Boolean, + isChannel: Boolean, + memberIds: Set[Int], + botId: Option[Int] +) diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupPeerCommandHandlers.scala b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupPeerCommandHandlers.scala index 3523f87e28..bca7533ee3 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupPeerCommandHandlers.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupPeerCommandHandlers.scala @@ -1,11 +1,10 @@ package im.actor.server.group -import akka.actor.Status import akka.http.scaladsl.util.FastFuture import akka.pattern.pipe import im.actor.api.rpc.PeersImplicits import im.actor.server.dialog.DialogCommands._ -import im.actor.server.group.GroupErrors.NotAMember +import im.actor.server.group.GroupErrors.{ NotAMember, NotAdmin } import im.actor.server.model.Peer import scala.concurrent.Future @@ -17,8 +16,18 @@ trait GroupPeerCommandHandlers extends PeersImplicits { protected def incomingMessage(state: GroupPeerState, sm: SendMessage): Unit = { val senderUserId = sm.getOrigin.id - (withMemberIds(groupId) { (memberIds, _, optBot) ⇒ - if (canSend(memberIds, optBot, senderUserId)) { + + val result: Future[SendMessageAck] = groupExt.canSendMessage(groupId, senderUserId) flatMap { + case CanSendMessageInfo(true, isChannel, memberIds, optBotId) ⇒ + val (ack, smUpdated) = if (isChannel) { + optBotId map { botId ⇒ + val botPeer = Peer.privat(botId) + SendMessageAck().withUpdatedSender(botPeer) → sm.withOrigin(botPeer) + } getOrElse (SendMessageAck() → sm) + } else { + SendMessageAck() → sm + } + val receiverIds = sm.forUserId match { case Some(id) if memberIds.contains(id) ⇒ Seq(id) case _ ⇒ memberIds - senderUserId @@ -26,18 +35,23 @@ trait GroupPeerCommandHandlers extends PeersImplicits { for { _ ← Future.traverse(receiverIds) { userId ⇒ - dialogExt.ackSendMessage(Peer.privat(userId), sm) + dialogExt.ackSendMessage(Peer.privat(userId), smUpdated) } } yield { self ! LastSenderIdChanged(senderUserId) - SendMessageAck() + ack } - } else FastFuture.successful(Status.Failure(NotAMember)) - } recover { - case e ⇒ - log.error(e, "Failed to send message") - throw e - }) pipeTo sender() + case CanSendMessageInfo(false, true, _, _) ⇒ + FastFuture.failed(NotAdmin) + case CanSendMessageInfo(false, _, _, _) ⇒ + FastFuture.failed(NotAMember) + } + + result onFailure { + case e: Exception ⇒ log.error(e, "Failed to send message") + } + + result pipeTo sender() } protected def messageReceived(state: GroupPeerState, mr: MessageReceived) = { diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupProcessor.scala b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupProcessor.scala index fb053f1d04..dd6b8954a7 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupProcessor.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupProcessor.scala @@ -1,24 +1,31 @@ package im.actor.server.group import java.time.Instant +import java.util.concurrent.TimeUnit import akka.actor.{ ActorRef, ActorSystem, Props, ReceiveTimeout, Status } import akka.cluster.sharding.ShardRegion import akka.http.scaladsl.util.FastFuture +import com.github.benmanes.caffeine.cache.{ Cache, Caffeine } +import im.actor.api.rpc.collections.{ ApiInt32Value, ApiMapValue, ApiMapValueItem, ApiStringValue } import im.actor.api.rpc.peers.{ ApiPeer, ApiPeerType } +import im.actor.concurrent.ActorFutures import im.actor.serialization.ActorSerializer -import im.actor.server.cqrs.{ Processor, TaggedEvent } +import im.actor.server.cqrs.{ Event, Processor, TaggedEvent } import im.actor.server.db.DbExtension -import im.actor.server.dialog.{ DialogEnvelope, DialogExtension } -import im.actor.server.group.GroupErrors.{ GroupIdAlreadyExists, GroupNotFound } +import im.actor.server.dialog._ +import im.actor.server.group.GroupErrors._ import im.actor.server.group.GroupCommands._ +import im.actor.server.group.GroupExt.Value.{ BoolValue, StringValue } import im.actor.server.group.GroupQueries._ +import im.actor.server.names.GlobalNamesStorageKeyValueStorage import im.actor.server.sequence.SeqUpdatesExtension import im.actor.server.user.UserExtension import scala.concurrent.duration._ import scala.concurrent.{ ExecutionContext, Future } +//TODO: maybe add dateMillis trait GroupEvent extends TaggedEvent { val ts: Instant @@ -37,14 +44,20 @@ object GroupProcessor { 20005 → classOf[GroupCommands.Kick], 20006 → classOf[GroupCommands.Leave], 20010 → classOf[GroupCommands.UpdateAvatar], - // 20011 → classOf[GroupCommands.MakePublic], - // 20012 → classOf[GroupCommands.MakePublicAck], 20013 → classOf[GroupCommands.UpdateTitle], 20015 → classOf[GroupCommands.UpdateTopic], 20016 → classOf[GroupCommands.UpdateAbout], 20017 → classOf[GroupCommands.MakeUserAdmin], 20018 → classOf[GroupCommands.RevokeIntegrationToken], 20020 → classOf[GroupCommands.RevokeIntegrationTokenAck], + 20021 → classOf[GroupCommands.TransferOwnership], + 20022 → classOf[GroupCommands.UpdateShortName], + 20023 → classOf[GroupCommands.DismissUserAdmin], + 20024 → classOf[GroupCommands.UpdateAdminSettings], + 20025 → classOf[GroupCommands.MakeHistoryShared], + 20026 → classOf[GroupCommands.DeleteGroup], + 20027 → classOf[GroupCommands.AddExt], + 20028 → classOf[GroupCommands.RemoveExt], 21001 → classOf[GroupQueries.GetIntegrationToken], 21002 → classOf[GroupQueries.GetIntegrationTokenResponse], @@ -54,12 +67,19 @@ object GroupProcessor { 21006 → classOf[GroupQueries.GetMembersResponse], 21007 → classOf[GroupQueries.GetApiStruct], 21008 → classOf[GroupQueries.GetApiStructResponse], - 21009 → classOf[GroupQueries.IsPublic], - 21010 → classOf[GroupQueries.IsPublicResponse], 21012 → classOf[GroupQueries.GetAccessHash], 21013 → classOf[GroupQueries.GetAccessHashResponse], 21014 → classOf[GroupQueries.IsHistoryShared], 21015 → classOf[GroupQueries.IsHistorySharedResponse], + 21016 → classOf[GroupQueries.GetTitle], + 21017 → classOf[GroupQueries.LoadMembers], + 21018 → classOf[GroupQueries.GetApiFullStruct], + 21019 → classOf[GroupQueries.CanSendMessage], + 21020 → classOf[GroupQueries.CanSendMessageResponse], + 21021 → classOf[GroupQueries.LoadAdminSettings], + 21022 → classOf[GroupQueries.LoadAdminSettingsResponse], + 21023 → classOf[GroupQueries.IsChannel], + 21024 → classOf[GroupQueries.IsChannelResponse], 22003 → classOf[GroupEvents.UserInvited], 22004 → classOf[GroupEvents.UserJoined], @@ -74,7 +94,15 @@ object GroupProcessor { 22013 → classOf[GroupEvents.TopicUpdated], 22015 → classOf[GroupEvents.UserBecameAdmin], 22016 → classOf[GroupEvents.IntegrationTokenRevoked], - 22017 → classOf[GroupEvents.OwnerChanged] + 22017 → classOf[GroupEvents.OwnerChanged], + 22018 → classOf[GroupEvents.ShortNameUpdated], + 22019 → classOf[GroupEvents.AdminSettingsUpdated], + 22020 → classOf[GroupEvents.AdminStatusChanged], + 22021 → classOf[GroupEvents.HistoryBecameShared], + 22022 → classOf[GroupEvents.GroupDeleted], + 22023 → classOf[GroupEvents.MembersBecameAsync], + 22024 → classOf[GroupEvents.ExtAdded], + 22025 → classOf[GroupEvents.ExtRemoved] ) def persistenceIdFor(groupId: Int): String = s"Group-${groupId}" @@ -85,6 +113,7 @@ object GroupProcessor { //FIXME: snapshots!!! private[group] final class GroupProcessor extends Processor[GroupState] + with ActorFutures with GroupCommandHandlers with GroupQueryHandlers { @@ -96,6 +125,14 @@ private[group] final class GroupProcessor protected val userExt = UserExtension(system) protected var integrationStorage: IntegrationTokensWriteOps = _ + protected val globalNamesStorage = new GlobalNamesStorageKeyValueStorage + + // short living cache to store member's names when user loads group members + protected implicit val memberNamesCache: Cache[java.lang.Integer, Future[String]] = + Caffeine.newBuilder() + .expireAfterAccess(3, TimeUnit.MINUTES) + .maximumSize(Long.MaxValue) + .build[java.lang.Integer, Future[String]] protected val groupId = self.path.name.toInt protected val apiGroupPeer = ApiPeer(ApiPeerType.Group, groupId) @@ -107,6 +144,7 @@ private[group] final class GroupProcessor case c: Create if state.isNotCreated ⇒ create(c) case _: Create ⇒ sender() ! Status.Failure(GroupIdAlreadyExists(groupId)) case _: GroupCommand if state.isNotCreated ⇒ sender() ! Status.Failure(GroupNotFound(groupId)) + case _: GroupCommand if state.isDeleted ⇒ sender() ! Status.Failure(GroupAlreadyDeleted(groupId)) // members actions case i: Invite ⇒ invite(i) @@ -119,20 +157,26 @@ private[group] final class GroupProcessor case u: UpdateTitle ⇒ updateTitle(u) case u: UpdateTopic ⇒ updateTopic(u) case u: UpdateAbout ⇒ updateAbout(u) + case u: UpdateShortName ⇒ updateShortName(u) + case a: AddExt ⇒ addExt(a) + case r: RemoveExt ⇒ removeExt(r) // admin actions case r: RevokeIntegrationToken ⇒ revokeIntegrationToken(r) case m: MakeUserAdmin ⇒ makeUserAdmin(m) + case d: DismissUserAdmin ⇒ dismissUserAdmin(d) case t: TransferOwnership ⇒ transferOwnership(t) - - // termination actions - case StopProcessor ⇒ context stop self - case ReceiveTimeout ⇒ context.parent ! ShardRegion.Passivate(stopMessage = StopProcessor) + case s: UpdateAdminSettings ⇒ updateAdminSettings(s) + case m: MakeHistoryShared ⇒ makeHistoryShared(m) + case d: DeleteGroup ⇒ deleteGroup(d) // dialogs envelopes coming through group. case de: DialogEnvelope ⇒ groupPeerActor forward de.getAllFields.values.head + // actor's lifecycle + case StopProcessor ⇒ context stop self + case ReceiveTimeout ⇒ context.parent ! ShardRegion.Passivate(stopMessage = StopProcessor) } // TODO: add backoff @@ -142,17 +186,43 @@ private[group] final class GroupProcessor } protected def handleQuery: PartialFunction[Any, Future[Any]] = { - case _: GroupQuery if state.isNotCreated ⇒ FastFuture.failed(GroupNotFound(groupId)) - case GetAccessHash() ⇒ getAccessHash - case GetTitle() ⇒ getTitle - case GetIntegrationToken(optClient) ⇒ getIntegrationToken(optClient) - case GetMembers() ⇒ getMembers - case LoadMembers(clientUserId, limit, offset) ⇒ loadMembers(clientUserId, limit, offset) - case IsPublic() ⇒ isPublic - case IsHistoryShared() ⇒ isHistoryShared - case GetApiStruct(clientUserId) ⇒ getApiStruct(clientUserId) - case GetApiFullStruct(clientUserId) ⇒ getApiFullStruct(clientUserId) - case CheckAccessHash(accessHash) ⇒ checkAccessHash(accessHash) + case _: GroupQuery if state.isNotCreated ⇒ FastFuture.failed(GroupNotFound(groupId)) + // case _: GroupQuery if state.isDeleted ⇒ FastFuture.failed(GroupAlreadyDeleted(groupId)) // TODO: figure out how to propperly handle group deletion + case GetAccessHash() ⇒ getAccessHash + case GetTitle() ⇒ getTitle + case GetIntegrationToken(optClient) ⇒ getIntegrationToken(optClient) + case GetMembers() ⇒ getMembers + case LoadMembers(clientUserId, limit, offset) ⇒ loadMembers(clientUserId, limit, offset) + case IsChannel() ⇒ isChannel + case IsHistoryShared() ⇒ isHistoryShared + case GetApiStruct(clientUserId, loadGroupMembers) ⇒ getApiStruct(clientUserId, loadGroupMembers) + case GetApiFullStruct(clientUserId) ⇒ getApiFullStruct(clientUserId) + case CheckAccessHash(accessHash) ⇒ checkAccessHash(accessHash) + case CanSendMessage(clientUserId) ⇒ canSendMessage(clientUserId) + case LoadAdminSettings(clientUserId) ⇒ loadAdminSettings(clientUserId) + } + + protected def extToApi(exts: Seq[GroupExt]): ApiMapValue = { + ApiMapValue( + exts.toVector map { + case GroupExt(key, BoolValue(b)) ⇒ ApiMapValueItem(key, ApiInt32Value(if (b) 1 else 0)) + case GroupExt(key, StringValue(s)) ⇒ ApiMapValueItem(key, ApiStringValue(s)) + } + ) + } + + override def afterCommit(e: Event) = { + super.afterCommit(e) + if (recoveryFinished) { + // can't make calls in group with more than 25 members + if (state.membersCount == 26) { + updateCanCall(state) + } + // from 50+ members we make group with async members + if (!state.isAsyncMembers && state.membersCount >= 50) { + makeMembersAsync() + } + } } def persistenceId: String = GroupProcessor.persistenceIdFor(groupId) diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupQueryHandlers.scala b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupQueryHandlers.scala index 517d00e127..65955c7ff9 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupQueryHandlers.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupQueryHandlers.scala @@ -5,8 +5,13 @@ import akka.stream.ActorMaterializer import akka.stream.scaladsl.Source import com.google.protobuf.ByteString import com.google.protobuf.wrappers.Int32Value -import im.actor.api.rpc.groups.{ ApiGroup, ApiGroupFull, ApiGroupType, ApiMember } +import im.actor.api.rpc.groups._ +import im.actor.server.group.GroupErrors.{ IncorrectGroupType, NoPermission, NotOwner } import im.actor.server.group.GroupQueries._ +import im.actor.server.group.GroupType.{ Channel, General, Unrecognized } +import im.actor.util.cache.CacheHelpers.withCachedFuture + +import scala.concurrent.Future trait GroupQueryHandlers { self: GroupProcessor ⇒ @@ -19,51 +24,80 @@ trait GroupQueryHandlers { protected def getTitle = FastFuture.successful(GetTitleResponse(state.title)) - protected def getIntegrationToken(optUserId: Option[Int]) = { + protected def getIntegrationToken(optUserId: Option[Int]): Future[GetIntegrationTokenResponse] = { val canViewToken = optUserId.forall(state.isAdmin) - FastFuture.successful(GetIntegrationTokenResponse( - if (canViewToken) state.bot.map(_.token) else None - )) + val allowedToView = optUserId.forall(state.isMember) + if (allowedToView) { + FastFuture.successful(GetIntegrationTokenResponse( + if (canViewToken) state.bot.map(_.token) else None + )) + } else { + FastFuture.failed(NoPermission) + } } - protected def getMembers = + //TODO: This is internal server API. Properly name it, for example `getMembersInternal` + protected def getMembers: Future[GetMembersResponse] = FastFuture.successful { GetMembersResponse( - memberIds = state.members.keySet.toSeq, + memberIds = state.memberIds.toSeq, invitedUserIds = state.invitedUserIds.toSeq, botId = state.bot.map(_.userId) ) } - protected def loadMembers(clientUserId: Int, limit: Int, offsetBs: Option[ByteString]) = { - implicit val mat = ActorMaterializer() - val offset = offsetBs map (_.toByteArray) map (Int32Value.parseFrom(_).value) getOrElse 0 - - for { - (userIds, nextOffset) ← Source(state.members.keySet) - .mapAsync(1)(userId ⇒ userExt.getName(userId, clientUserId) map (userId → _)) - .runFold(Vector.empty[(Int, String)])(_ :+ _) map { users ⇒ - val tail = users.sortBy(_._2).map(_._1).drop(offset) - val nextOffset = if (tail.length > limit) Some(Int32Value(offset + limit).toByteArray) else None - (tail.take(limit), nextOffset) - } - } yield LoadMembersResponse( - userIds = userIds, - offset = nextOffset map ByteString.copyFrom - ) + protected def loadMembers(clientUserId: Int, limit: Int, offsetBs: Option[ByteString]): Future[LoadMembersResponse] = { + def load = { + implicit val mat = ActorMaterializer() + val offset = offsetBs map (_.toByteArray) map (Int32Value.parseFrom(_).value) getOrElse 0 + + for { + (members, nextOffset) ← Source(state.members) + .mapAsync(1) { + case (userId, member) ⇒ + withCachedFuture[java.lang.Integer, String](userId) { + userExt.getName(userId, clientUserId) + } map { name ⇒ member → name } + } + .runFold(Vector.empty[(Member, String)])(_ :+ _) map { users ⇒ + val tail = users.sortBy(_._2).map(_._1).drop(offset) + val nextOffset = if (tail.length > limit) Some(Int32Value(offset + limit).toByteArray) else None + (tail.take(limit), nextOffset) + } + } yield LoadMembersResponse( + members = members map { + case Member(userId, inviterUserId, invitedAt, isAdmin) ⇒ + GroupMember( + userId, + inviterUserId, + invitedAt.toEpochMilli, + isAdmin + ) + }, + offset = nextOffset map ByteString.copyFrom + ) + } + + state.groupType match { + case General ⇒ load + case Channel ⇒ + if (state.isAdmin(clientUserId)) load + else FastFuture.successful(LoadMembersResponse(Seq.empty, offsetBs)) + case Unrecognized(v) ⇒ throw IncorrectGroupType(v) + } } - protected def isPublic = - FastFuture.successful(IsPublicResponse(isPublic = state.typ == GroupType.Public)) + protected def isChannel = + FastFuture.successful(IsChannelResponse(state.groupType.isChannel)) protected def isHistoryShared = FastFuture.successful(IsHistorySharedResponse(state.isHistoryShared)) //TODO: add ext! //TODO: what if state changes during request? - protected def getApiStruct(clientUserId: Int) = { + protected def getApiStruct(clientUserId: Int, loadGroupMembers: Boolean) = { val isMember = state.isMember(clientUserId) - val apiMembers = getApiMembers(state, clientUserId) + val (members, count) = membersAndCount(state, clientUserId) FastFuture.successful { GetApiStructResponse( @@ -74,19 +108,21 @@ trait GroupQueryHandlers { avatar = state.avatar, isMember = Some(isMember), creatorUserId = state.creatorUserId, - members = apiMembers, - createDate = extractCreatedMillis(state), + members = if (loadGroupMembers) members else Vector.empty, + createDate = extractCreatedAtMillis(state), isAdmin = Some(state.isAdmin(clientUserId)), theme = state.topic, about = state.about, isHidden = Some(state.isHidden), - ext = None, - membersCount = Some(apiMembers.size), - groupType = Some(state.typ match { - case GroupType.Channel ⇒ ApiGroupType.CHANNEL - case GroupType.General | GroupType.Public | GroupType.Unrecognized(_) ⇒ ApiGroupType.GROUP + ext = Some(extToApi(state.exts)), + membersCount = Some(count), + groupType = Some(state.groupType match { + case Channel ⇒ ApiGroupType.CHANNEL + case General ⇒ ApiGroupType.GROUP + case Unrecognized(v) ⇒ throw IncorrectGroupType(v) }), - canSendMessage = None + permissions = Some(state.permissions.groupFor(clientUserId)), + isDeleted = Some(state.isDeleted) ) ) } @@ -100,14 +136,14 @@ trait GroupQueryHandlers { groupId, theme = state.topic, about = state.about, - ownerUserId = state.creatorUserId, - createDate = extractCreatedMillis(state), + ownerUserId = state.getShowableOwner(clientUserId), + createDate = extractCreatedAtMillis(state), ext = None, - canViewMembers = Some(state.canViewMembers(clientUserId)), - canInvitePeople = Some(state.canInvitePeople(clientUserId)), isSharedHistory = Some(state.isHistoryShared), - isAsyncMembers = Some(state.members.size > 100), - members = getApiMembers(state, clientUserId) + isAsyncMembers = Some(state.isAsyncMembers), + members = membersAndCount(state, clientUserId)._1, + shortName = state.shortName, + permissions = Some(state.permissions.fullFor(clientUserId)) ) ) } @@ -115,15 +151,66 @@ trait GroupQueryHandlers { protected def checkAccessHash(hash: Long) = FastFuture.successful(CheckAccessHashResponse(isCorrect = state.accessHash == hash)) - private def extractCreatedMillis(group: GroupState): Long = + protected def canSendMessage(clientUserId: Int): Future[CanSendMessageResponse] = + FastFuture.successful { + val canSend = state.bot.exists(_.userId == clientUserId) || { + state.groupType match { + case General ⇒ state.isMember(clientUserId) + case Channel ⇒ state.isAdmin(clientUserId) + case Unrecognized(v) ⇒ throw IncorrectGroupType(v) + } + } + CanSendMessageResponse( + canSend = canSend, + isChannel = state.groupType.isChannel, + memberIds = state.memberIds.toSeq, + botId = state.bot.map(_.userId) + ) + } + + protected def loadAdminSettings(clientUserId: Int): Future[LoadAdminSettingsResponse] = { + if (state.permissions.canEditAdminSettings(clientUserId)) { + FastFuture.successful { + LoadAdminSettingsResponse( + ApiAdminSettings( + showAdminsToMembers = state.adminSettings.showAdminsToMembers, + canMembersInvite = state.adminSettings.canMembersInvite, + canMembersEditGroupInfo = state.adminSettings.canMembersEditGroupInfo, + canAdminsEditGroupInfo = state.adminSettings.canAdminsEditGroupInfo, + showJoinLeaveMessages = state.adminSettings.showJoinLeaveMessages + ) + ) + } + } else { + FastFuture.failed(NotOwner) + } + } + + private def extractCreatedAtMillis(group: GroupState): Long = group.createdAt.map(_.toEpochMilli).getOrElse(throw new RuntimeException("No date created provided for group!")) - private def getApiMembers(group: GroupState, clientUserId: Int) = + /** + * Return group members, and number of members. + * If `clientUserId` is not a group member, return empty members list and 0 members count + * If group is group with async members - return list with single client user and real members count + * If group is regular group - return all group members and real members count + */ + private def membersAndCount(group: GroupState, clientUserId: Int): (Vector[ApiMember], Int) = { + def apiMembers = group.members.toVector map { + case (_, m) ⇒ + ApiMember(m.userId, m.inviterUserId, m.invitedAt.toEpochMilli, Some(m.isAdmin)) + } + if (state.isMember(clientUserId)) { - group.members.toVector map { - case (_, m) ⇒ - ApiMember(m.userId, m.inviterUserId, m.invitedAt.toEpochMilli, Some(m.isAdmin)) + if (state.isAsyncMembers) { + // compatibility with old clients + apiMembers.find(_.userId == clientUserId).toVector → group.membersCount + } else { + apiMembers → group.membersCount } - } else Vector.empty[ApiMember] + } else { + Vector.empty[ApiMember] → 0 + } + } } diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupState.scala b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupState.scala index 128d3dac95..fa42616a9b 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupState.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupState.scala @@ -3,16 +3,19 @@ package im.actor.server.group import java.time.Instant import akka.persistence.SnapshotMetadata +import im.actor.api.rpc.groups.ApiAdminSettings import im.actor.api.rpc.misc.ApiExtension import im.actor.server.cqrs.{ Event, ProcessorState } import im.actor.server.file.Avatar +import im.actor.server.group.GroupErrors.IncorrectGroupType import im.actor.server.group.GroupEvents._ +import im.actor.server.group.GroupType.{ Channel, General, Unrecognized } private[group] final case class Member( userId: Int, inviterUserId: Int, invitedAt: Instant, - isAdmin: Boolean + isAdmin: Boolean // TODO: remove, use separate admins list instead ) private[group] final case class Bot( @@ -20,6 +23,54 @@ private[group] final case class Bot( token: String ) +object AdminSettings { + val PlainDefault = AdminSettings( + showAdminsToMembers = true, + canMembersInvite = true, + canMembersEditGroupInfo = true, + canAdminsEditGroupInfo = true, + showJoinLeaveMessages = true + ) + + val ChannelsDefault = AdminSettings( + showAdminsToMembers = false, + canMembersInvite = false, + canMembersEditGroupInfo = false, + canAdminsEditGroupInfo = true, + showJoinLeaveMessages = false // TODO: figure it out. We don't use it by default + ) + + // format: OFF + def apiToBitMask(settings: ApiAdminSettings): Int = { + def toInt(b: Boolean) = if (b) 1 else 0 + + (toInt(settings.showAdminsToMembers) << 0) + + (toInt(settings.canMembersInvite) << 1) + + (toInt(settings.canMembersEditGroupInfo) << 2) + + (toInt(settings.canAdminsEditGroupInfo) << 3) + + (toInt(settings.showJoinLeaveMessages) << 4) + } + + def fromBitMask(mask: Int): AdminSettings = { + AdminSettings( + showAdminsToMembers = (mask & (1 << 0)) != 0, + canMembersInvite = (mask & (1 << 1)) != 0, + canMembersEditGroupInfo = (mask & (1 << 2)) != 0, + canAdminsEditGroupInfo = (mask & (1 << 3)) != 0, + showJoinLeaveMessages = (mask & (1 << 4)) != 0 + ) + } + // format: ON +} + +private[group] final case class AdminSettings( + showAdminsToMembers: Boolean, // 1 + canMembersInvite: Boolean, // 2 + canMembersEditGroupInfo: Boolean, // 4 + canAdminsEditGroupInfo: Boolean, // 8 + showJoinLeaveMessages: Boolean // 16 +) + private[group] object GroupState { def empty: GroupState = GroupState( @@ -32,16 +83,21 @@ private[group] object GroupState { about = None, avatar = None, topic = None, - typ = GroupType.General, + shortName = None, + groupType = GroupType.General, isHidden = false, isHistoryShared = false, + isAsyncMembers = false, members = Map.empty, invitedUserIds = Set.empty, accessHash = 0L, + adminSettings = AdminSettings.PlainDefault, bot = None, + deletedAt = None, //??? - extensions = Map.empty + internalExtensions = Map.empty, + exts = Seq.empty ) } @@ -58,23 +114,30 @@ private[group] final case class GroupState( about: Option[String], avatar: Option[Avatar], topic: Option[String], - typ: GroupType, + shortName: Option[String], + groupType: GroupType, isHidden: Boolean, isHistoryShared: Boolean, + isAsyncMembers: Boolean, // members info members: Map[Int, Member], invitedUserIds: Set[Int], //security and etc. - accessHash: Long, - bot: Option[Bot], - extensions: Map[Int, Array[Byte]] //or should it be sequence??? + accessHash: Long, + adminSettings: AdminSettings, + bot: Option[Bot], + deletedAt: Option[Instant], + internalExtensions: Map[Int, Array[Byte]], + exts: Seq[GroupExt] ) extends ProcessorState[GroupState] { - def memberIds = members.keySet //TODO: Maybe lazy val. immutable anyway + lazy val memberIds = members.keySet + + lazy val adminIds = (members filter (_._2.isAdmin == true)).keySet - def membersCount = members.size //TODO: Maybe lazy val. immutable anyway + lazy val membersCount = members.size def isMember(userId: Int): Boolean = members.contains(userId) @@ -86,24 +149,28 @@ private[group] final case class GroupState( def isAdmin(userId: Int): Boolean = members.get(userId) exists (_.isAdmin) + // owner will be super-admin in case of channels def isOwner(userId: Int): Boolean = userId == ownerUserId def isExUser(userId: Int): Boolean = exUserIds.contains(userId) - def canViewMembers(clientUserId: Int) = - (typ.isGeneral || typ.isPublic) && isMember(clientUserId) + val isNotCreated = createdAt.isEmpty - def canInvitePeople(clientUserId: Int) = isMember(clientUserId) + val isCreated = createdAt.nonEmpty - def canViewMembers(group: GroupState, userId: Int) = - (group.typ.isGeneral || group.typ.isPublic) && isMember(userId) + val isDeleted = deletedAt.nonEmpty - def isNotCreated = createdAt.isEmpty //TODO: Maybe val. immutable anyway - - def isCreated = createdAt.nonEmpty //TODO: Maybe val. immutable anyway + def getShowableOwner(clientUserId: Int): Option[Int] = + groupType match { + case General ⇒ Some(creatorUserId) + case Channel ⇒ if (isAdmin(clientUserId)) Some(creatorUserId) else None + case Unrecognized(v) ⇒ throw IncorrectGroupType(v) + } override def updated(e: Event): GroupState = e match { case evt: Created ⇒ + val typeOfGroup = evt.typ.getOrElse(GroupType.General) + val isMemberAsync = typeOfGroup.isChannel this.copy( id = evt.groupId, createdAt = Some(evt.ts), @@ -113,9 +180,11 @@ private[group] final case class GroupState( about = None, avatar = None, topic = None, - typ = evt.typ.getOrElse(GroupType.General), + shortName = None, + groupType = typeOfGroup, isHidden = evt.isHidden getOrElse false, isHistoryShared = evt.isHistoryShared getOrElse false, + isAsyncMembers = isMemberAsync, members = ( evt.userIds map { userId ⇒ userId → @@ -129,8 +198,11 @@ private[group] final case class GroupState( ).toMap, invitedUserIds = evt.userIds.filterNot(_ == evt.creatorUserId).toSet, accessHash = evt.accessHash, + adminSettings = + if (typeOfGroup.isChannel) AdminSettings.ChannelsDefault + else AdminSettings.PlainDefault, bot = None, - extensions = (evt.extensions map { //TODO: validate is it right? + internalExtensions = (evt.extensions map { //TODO: validate is it right? case ApiExtension(extId, data) ⇒ extId → data }).toMap @@ -179,18 +251,13 @@ private[group] final case class GroupState( this.copy(avatar = newAvatar) case TitleUpdated(_, newTitle) ⇒ this.copy(title = newTitle) - case BecamePublic(_) ⇒ - this.copy( - typ = GroupType.Public, - isHistoryShared = true - ) case AboutUpdated(_, newAbout) ⇒ this.copy(about = newAbout) case TopicUpdated(_, newTopic) ⇒ this.copy(topic = newTopic) - case UserBecameAdmin(_, userId, _) ⇒ + case AdminStatusChanged(_, userId, isAdmin) ⇒ this.copy( - members = members.updated(userId, members(userId).copy(isAdmin = true)) + members = members.updated(userId, members(userId).copy(isAdmin = isAdmin)) ) case IntegrationTokenRevoked(_, newToken) ⇒ this.copy( @@ -198,8 +265,242 @@ private[group] final case class GroupState( ) case OwnerChanged(_, userId) ⇒ this.copy(ownerUserId = userId) + case ShortNameUpdated(_, newShortName) ⇒ + this.copy(shortName = newShortName) + case AdminSettingsUpdated(_, bitMask) ⇒ + this.copy(adminSettings = AdminSettings.fromBitMask(bitMask)) + case HistoryBecameShared(_, _) ⇒ + this.copy(isHistoryShared = true) + case MembersBecameAsync(_) ⇒ + this.copy(isAsyncMembers = true) + case GroupDeleted(ts, _) ⇒ + // FIXME: don't implement snapshots, before figure out deleted groups behavior + this.copy( + deletedAt = Some(ts), + + members = Map.empty, + invitedUserIds = Set.empty, + exUserIds = Set.empty, + bot = None, + topic = None, + about = None, + avatar = None, + adminSettings = + if (groupType.isChannel) AdminSettings.ChannelsDefault + else AdminSettings.PlainDefault + ) + case ExtAdded(_, ext) ⇒ + if (exts.contains(ext)) + this + else + this.copy(exts = exts :+ ext) + case ExtRemoved(_, key) ⇒ + this.copy(exts = exts.filterNot(_.key == key)) + // deprecated events + case UserBecameAdmin(_, userId, _) ⇒ + this.copy( + members = members.updated(userId, members(userId).copy(isAdmin = true)) + ) + case BecamePublic(_) ⇒ + this.copy(isHistoryShared = true) } // TODO: real snapshot def withSnapshot(metadata: SnapshotMetadata, snapshot: Any): GroupState = this + + object permissions { + + /////////////////////////// + // General permissions // + /////////////////////////// + + /** + * @note check up to date doc in im.actor.api.rpc.groups.ApiGroup + * + * Group permissions bits: + * 0 - canSendMessage. Default is FALSE. + * 1 - canClear. Default is FALSE. + * 2 - canLeave. Default is FALSE. + * 3 - canDelete. Default is FALSE. + * 4 - canJoin. Default is FALSE. + * 5 - canViewInfo. Default is FALSE. + */ + // TODO: add ApiGroupFullPermissions + def groupFor(userId: Int): Long = { + ((toInt(canSendMessage(userId)) << 0) + + (toInt(canClear(userId)) << 1) + + (toInt(canLeave(userId)) << 2) + + (toInt(canDelete(userId)) << 3) + + (toInt(canJoin(userId)) << 4) + + (toInt(canViewInfo(userId)) << 5)).toLong + } + + /** + * bot can send messages in all groups + * in general/public group only members can send messages + * in channels only owner and admins can send messages + */ + private def canSendMessage(clientUserId: Int) = + { + groupType match { + case General ⇒ isMember(clientUserId) + case Channel ⇒ isAdmin(clientUserId) || isOwner(clientUserId) + case Unrecognized(v) ⇒ throw IncorrectGroupType(v) + } + } || bot.exists(_.userId == clientUserId) + + // if history shared, only owner can clear, everyone otherwise + private def canClear(clientUserId: Int): Boolean = !isHistoryShared || isOwner(clientUserId) + + /** + * for now, owner can't leave group. + * He can either transfer ownership and leave group + * or delete group completely. + */ + def canLeave(clientUserId: Int): Boolean = !isOwner(clientUserId) + + // only owner can delete group + def canDelete(clientUserId: Int): Boolean = isOwner(clientUserId) + + // anyone can join in group with shared history + def canJoin(clientUserId: Int): Boolean = isHistoryShared + + // if history shared - anyone can view info + // only members can view info in private groups + def canViewInfo(clientUserId: Int): Boolean = isHistoryShared || isMember(clientUserId) + + //////////////////////////// + // Full group permissions // + //////////////////////////// + + /** + * @note check up to date doc at im.actor.api.rpc.groups.ApiGroupFull + * + * Full group permissions bits: + * 0 - canEditInfo. Default is FALSE. + * 1 - canViewMembers. Default is FALSE. + * 2 - canInviteMembers. Default is FALSE. + * 3 - canInviteViaLink. Default is FALSE. + * 4 - canCall. Default is FALSE. + * 5 - canEditAdminSettings. Default is FALSE. + * 6 - canViewAdmins. Default is FALSE. + * 7 - canEditAdmins. Default is FALSE. + * 8 - canKickInvited. Default is FALSE. + * 9 - canKickAnyone. Default is FALSE. + * 10 - canEditForeign. Default is FALSE. + * 11 - canDeleteForeign. Default is FALSE. + */ + // TODO: add ApiGroupFullPermissions + def fullFor(userId: Int): Long = { + ( + (toInt(canEditInfo(userId)) << 0) + + (toInt(canViewMembers(userId)) << 1) + + (toInt(canInviteMembers(userId)) << 2) + + (toInt(canInviteViaLink(userId)) << 3) + + (toInt(canCall) << 4) + + (toInt(canEditAdminSettings(userId)) << 5) + + (toInt(canViewAdmins(userId)) << 6) + + (toInt(canEditAdmins(userId)) << 7) + + (toInt(canKickInvited(userId)) << 8) + + (toInt(canKickAnyone(userId)) << 9) + + (toInt(canEditForeign(userId)) << 10) + + (toInt(canDeleteForeign(userId)) << 11) + ).toLong + } + + /** + * owner always can edit group info + * admin can edit group info, if canAdminsEditGroupInfo is true in admin settings + * any member can edit group info, if canMembersEditGroupInfo is true in admin settings + */ + def canEditInfo(clientUserId: Int): Boolean = + isOwner(clientUserId) || + (isAdmin(clientUserId) && adminSettings.canAdminsEditGroupInfo) || + (isMember(clientUserId) && adminSettings.canMembersEditGroupInfo) + + /** + * in general/public group, all members can view members + * in channels, owner and admins can view members + */ + def canViewMembers(clientUserId: Int) = + groupType match { + case General ⇒ isMember(clientUserId) + case Channel ⇒ isAdmin(clientUserId) || isOwner(clientUserId) + case Unrecognized(v) ⇒ throw IncorrectGroupType(v) + } + + /** + * owner and admins always can invite new members + * regular members can invite new members if adminSettings.canMembersInvite is true + */ + def canInviteMembers(clientUserId: Int) = + isOwner(clientUserId) || + isAdmin(clientUserId) || + (isMember(clientUserId) && adminSettings.canMembersInvite) + + /** + * only owner and admins can invite via link + */ + private def canInviteViaLink(clientUserId: Int) = isOwner(clientUserId) || isAdmin(clientUserId) + + /** + * All members can call, if group has less than 25 members, and is not a channel + */ + def canCall = !groupType.isChannel && membersCount <= 25 + + // only owner can change admin settings + def canEditAdminSettings(clientUserId: Int): Boolean = isOwner(clientUserId) + + /** + * admins list is always visible to owner and admins + * admins list is visible to any member if showAdminsToMembers = true + */ + private def canViewAdmins(clientUserId: Int): Boolean = + isOwner(clientUserId) || isAdmin(clientUserId) || adminSettings.showAdminsToMembers + + // only owner and other admins can edit admins list + def canEditAdmins(clientUserId: Int): Boolean = + isOwner(clientUserId) || isAdmin(clientUserId) + + /** + * In General group members can kick people they invited + * In Channel only owner and admins can kick invited people + */ + def canKickInvited(userId: Int): Boolean = + groupType match { + case General ⇒ isMember(userId) + case Channel ⇒ isAdmin(userId) || isOwner(userId) + case Unrecognized(v) ⇒ throw IncorrectGroupType(v) + } + + /** + * Only owner and admins can kick anyone + */ + def canKickAnyone(userId: Int): Boolean = + isOwner(userId) || isAdmin(userId) + + /** + * Only owner and admins can edit foreign messages + */ + private def canEditForeign(userId: Int): Boolean = + isOwner(userId) || isAdmin(userId) + + /** + * Only owner and admins can delete foreign messages + */ + private def canDeleteForeign(userId: Int): Boolean = + isOwner(userId) || isAdmin(userId) + + //////////////////////////// + // Internal permissions // + //////////////////////////// + + // only owner can change short name + def canEditShortName(clientUserId: Int): Boolean = isOwner(clientUserId) + + // only owner can make history shared + def canMakeHistoryShared(clientUserId: Int): Boolean = isOwner(clientUserId) + + private def toInt(b: Boolean) = if (b) 1 else 0 + } } diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupUtils.scala b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupUtils.scala index 642b9e5935..ab0ea666b6 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupUtils.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/group/GroupUtils.scala @@ -12,7 +12,7 @@ object GroupUtils { def getUserIds(group: ApiGroup): Set[Int] = group.members.flatMap(m ⇒ Seq(m.userId, m.inviterUserId)).toSet + group.creatorUserId - def getUserIds(groups: Seq[ApiGroup]): Set[Int] = + private def getUserIds(groups: Seq[ApiGroup]): Set[Int] = groups.foldLeft(Set.empty[Int])(_ ++ getUserIds(_)) def getGroupsUsers(groupIds: Seq[Int], userIds: Seq[Int], clientUserId: Int, clientAuthId: Long)(implicit system: ActorSystem): Future[(Seq[ApiGroup], Seq[ApiUser])] = { @@ -20,7 +20,7 @@ object GroupUtils { for { groups ← Future.sequence(groupIds map (GroupExtension(system).getApiStruct(_, clientUserId))) memberIds = getUserIds(groups) - users ← Future.sequence((userIds.toSet ++ memberIds.toSet).filterNot(_ == 0) map (UserUtils.safeGetUser(_, clientUserId, clientAuthId))) map (_.flatten) + users ← Future.sequence((userIds.toSet ++ memberIds).filterNot(_ == 0) map (UserUtils.safeGetUser(_, clientUserId, clientAuthId))) map (_.flatten) } yield (groups, users.toSeq) } } diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/group/InfoCommandHandlers.scala b/actor-server/actor-core/src/main/scala/im/actor/server/group/InfoCommandHandlers.scala new file mode 100644 index 0000000000..bc42b1eebf --- /dev/null +++ b/actor-server/actor-core/src/main/scala/im/actor/server/group/InfoCommandHandlers.scala @@ -0,0 +1,365 @@ +package im.actor.server.group + +import java.time.Instant + +import akka.actor.Status +import akka.pattern.pipe +import akka.http.scaladsl.util.FastFuture +import com.github.ghik.silencer.silent +import im.actor.api.rpc.files.ApiAvatar +import im.actor.api.rpc.groups._ +import im.actor.server.file.{ Avatar, ImageUtils } +import im.actor.server.group.GroupCommands.{ AddExt, AddExtAck, MakeHistoryShared, RemoveExt, RemoveExtAck, UpdateAbout, UpdateAvatar, UpdateAvatarAck, UpdateShortName, UpdateTitle, UpdateTopic } +import im.actor.server.group.GroupErrors._ +import im.actor.server.group.GroupEvents.{ AboutUpdated, AvatarUpdated, ShortNameUpdated, TitleUpdated, TopicUpdated } +import im.actor.server.model.AvatarData +import im.actor.server.names.{ GlobalNameOwner, OwnerType } +import im.actor.server.persist.{ AvatarDataRepo, GroupRepo } +import im.actor.server.sequence.{ Optimization, SeqState, SeqStateDate } +import im.actor.util.misc.StringUtils + +import scala.concurrent.Future + +private[group] trait InfoCommandHandlers { + this: GroupProcessor ⇒ + + import im.actor.server.ApiConversions._ + + protected def updateAvatar(cmd: UpdateAvatar): Unit = { + if (!state.permissions.canEditInfo(cmd.clientUserId)) { + sender() ! noPermission + } else { + persist(AvatarUpdated(Instant.now, cmd.avatar)) { evt ⇒ + val newState = commit(evt) + + val dateMillis = evt.ts.toEpochMilli + val apiAvatar: Option[ApiAvatar] = cmd.avatar + val memberIds = newState.memberIds + + val updateNew = UpdateGroupAvatarChanged(groupId, apiAvatar) + val updateObsolete = UpdateGroupAvatarChangedObsolete(groupId, cmd.clientUserId, apiAvatar, dateMillis, cmd.randomId) + val serviceMessage = GroupServiceMessages.changedAvatar(apiAvatar) + + db.run(AvatarDataRepo.createOrUpdate(getAvatarData(cmd.avatar))) + val result: Future[UpdateAvatarAck] = for { + /////////////////////////// + // Groups V1 API updates // + /////////////////////////// + + _ ← seqUpdExt.broadcastClientUpdate(cmd.clientUserId, cmd.clientAuthId, memberIds, updateObsolete) + + /////////////////////////// + // Groups V2 API updates // + /////////////////////////// + + SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( + userId = cmd.clientUserId, + authId = cmd.clientAuthId, + bcastUserIds = memberIds - cmd.clientUserId, + update = updateNew + ) + SeqStateDate(_, _, date) ← dialogExt.sendServerMessage( + apiGroupPeer, + senderUserId = cmd.clientUserId, + senderAuthId = cmd.clientAuthId, + randomId = cmd.randomId, + message = serviceMessage, + deliveryTag = Some(Optimization.GroupV2) + ) + } yield UpdateAvatarAck(apiAvatar).withSeqStateDate(SeqStateDate(seq, state, date)) + + result pipeTo sender() + } + } + } + + protected def updateTitle(cmd: UpdateTitle): Unit = { + val title = cmd.title + if (!state.permissions.canEditInfo(cmd.clientUserId)) { + sender() ! noPermission + } else if (!isValidTitle(title)) { + sender() ! Status.Failure(InvalidTitle) + } else { + persist(TitleUpdated(Instant.now(), title)) { evt ⇒ + val newState = commit(evt) + + val dateMillis = evt.ts.toEpochMilli + val memberIds = newState.memberIds + + val updateNew = UpdateGroupTitleChanged(groupId, title) + val updateObsolete = UpdateGroupTitleChangedObsolete( + groupId, + userId = cmd.clientUserId, + title = title, + date = dateMillis, + randomId = cmd.randomId + ) + val serviceMessage = GroupServiceMessages.changedTitle(title) + val pushRules = seqUpdExt.pushRules(isFat = false, Some(PushTexts.titleChanged(newState.groupType))) + + //TODO: remove deprecated + db.run(GroupRepo.updateTitle(groupId, title, cmd.clientUserId, cmd.randomId, date = evt.ts): @silent) + + val result: Future[SeqStateDate] = for { + + /////////////////////////// + // Groups V1 API updates // + /////////////////////////// + + _ ← seqUpdExt.broadcastClientUpdate( + cmd.clientUserId, + cmd.clientAuthId, + memberIds - cmd.clientUserId, + updateObsolete, + pushRules + ) + + /////////////////////////// + // Groups V2 API updates // + /////////////////////////// + + SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( + userId = cmd.clientUserId, + authId = cmd.clientAuthId, + bcastUserIds = memberIds - cmd.clientUserId, + update = updateNew, + pushRules = pushRules + ) + SeqStateDate(_, _, date) ← dialogExt.sendServerMessage( + apiGroupPeer, + senderUserId = cmd.clientUserId, + senderAuthId = cmd.clientAuthId, + randomId = cmd.randomId, + message = serviceMessage, + deliveryTag = Some(Optimization.GroupV2) + ) + } yield SeqStateDate(seq, state, date) + + result pipeTo sender() + } + + } + } + + //TODO: who can update topic??? + protected def updateTopic(cmd: UpdateTopic): Unit = { + def isValidTopic(topic: Option[String]) = topic.forall(_.length < 255) + + val topic = trimToEmpty(cmd.topic) + + if (state.groupType.isChannel && !state.isAdmin(cmd.clientUserId)) { + sender() ! notAdmin + } else if (state.nonMember(cmd.clientUserId)) { + sender() ! notMember + } else if (!isValidTopic(topic)) { + sender() ! Status.Failure(TopicTooLong) + } else { + persist(TopicUpdated(Instant.now, topic)) { evt ⇒ + val newState = commit(evt) + + val dateMillis = evt.ts.toEpochMilli + val memberIds = newState.memberIds + + val updateNew = UpdateGroupTopicChanged(groupId, topic) + val updateObsolete = UpdateGroupTopicChangedObsolete( + groupId, + randomId = cmd.randomId, + userId = cmd.clientUserId, + topic = topic, + date = dateMillis + ) + val serviceMessage = GroupServiceMessages.changedTopic(topic) + val pushRules = seqUpdExt.pushRules(isFat = false, Some(PushTexts.topicChanged(newState.groupType))) + + //TODO: remove deprecated + db.run(GroupRepo.updateTopic(groupId, topic): @silent) + + val result: Future[SeqStateDate] = for { + + /////////////////////////// + // Groups V1 API updates // + /////////////////////////// + + _ ← seqUpdExt.broadcastClientUpdate( + cmd.clientUserId, + cmd.clientAuthId, + memberIds - cmd.clientUserId, + updateObsolete, + pushRules + ) + + /////////////////////////// + // Groups V2 API updates // + /////////////////////////// + + SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( + userId = cmd.clientUserId, + authId = cmd.clientAuthId, + bcastUserIds = memberIds - cmd.clientUserId, + update = updateNew, + pushRules = pushRules + ) + SeqStateDate(_, _, date) ← dialogExt.sendServerMessage( + apiGroupPeer, + senderUserId = cmd.clientUserId, + senderAuthId = cmd.clientAuthId, + randomId = cmd.randomId, + message = serviceMessage, + deliveryTag = Some(Optimization.GroupV2) + ) + } yield SeqStateDate(seq, state, date) + + result pipeTo sender() + } + } + } + + protected def updateAbout(cmd: UpdateAbout): Unit = { + def isValidAbout(about: Option[String]) = about.forall(_.length < 255) + + val about = trimToEmpty(cmd.about) + + if (!state.permissions.canEditInfo(cmd.clientUserId)) { + sender() ! noPermission + } else if (!isValidAbout(about)) { + sender() ! Status.Failure(AboutTooLong) + } else { + + persist(AboutUpdated(Instant.now, about)) { evt ⇒ + val newState = commit(evt) + + val memberIds = newState.memberIds + + val updateNew = UpdateGroupAboutChanged(groupId, about) + val updateObsolete = UpdateGroupAboutChangedObsolete(groupId, about) + val serviceMessage = GroupServiceMessages.changedAbout(about) + val pushRules = seqUpdExt.pushRules(isFat = false, Some(PushTexts.topicChanged(newState.groupType))) + + //TODO: remove deprecated + db.run(GroupRepo.updateAbout(groupId, about): @silent) + + val result: Future[SeqStateDate] = for { + + /////////////////////////// + // Groups V1 API updates // + /////////////////////////// + + _ ← seqUpdExt.broadcastClientUpdate( + cmd.clientUserId, + cmd.clientAuthId, + memberIds - cmd.clientUserId, + updateObsolete, + pushRules + ) + + /////////////////////////// + // Groups V2 API updates // + /////////////////////////// + + SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( + userId = cmd.clientUserId, + authId = cmd.clientAuthId, + bcastUserIds = memberIds - cmd.clientUserId, + update = updateNew, + pushRules = pushRules + ) + } yield SeqStateDate(seq, state, evt.ts.toEpochMilli) + + result pipeTo sender() + } + } + } + + protected def updateShortName(cmd: UpdateShortName): Unit = { + def isValidShortName(shortName: Option[String]) = shortName forall StringUtils.validGlobalName + + val oldShortName = state.shortName + val newShortName = trimToEmpty(cmd.shortName) + + if (!state.permissions.canEditShortName(cmd.clientUserId)) { + sender() ! noPermission + } else if (!isValidShortName(newShortName)) { + sender() ! Status.Failure(InvalidShortName) + } else if (oldShortName == newShortName) { + seqUpdExt.getSeqState(cmd.clientUserId, cmd.clientAuthId) pipeTo sender() + } else { + val replyTo = sender() + + val existsFu = newShortName map { name ⇒ + globalNamesStorage.exists(name) + } getOrElse FastFuture.successful(false) + + //TODO: timeout for this + onSuccess(existsFu) { exists ⇒ + if (exists) { + replyTo ! Status.Failure(ShortNameTaken) + } else { + // when user sets short name first time - we making group history shared + if (state.shortName.isEmpty && newShortName.nonEmpty && !state.isHistoryShared) { + context.parent ! + GroupEnvelope(groupId) + .withMakeHistoryShared(MakeHistoryShared(cmd.clientUserId, cmd.clientAuthId)) + } + + persist(ShortNameUpdated(Instant.now, newShortName)) { evt ⇒ + val newState = commit(evt) + + val memberIds = newState.memberIds + + val result: Future[SeqState] = for { + _ ← globalNamesStorage.updateOrRemove( + oldShortName, + newShortName, + GlobalNameOwner(OwnerType.Group, groupId) + ) + seqState ← seqUpdExt.broadcastClientUpdate( + userId = cmd.clientUserId, + authId = cmd.clientAuthId, + bcastUserIds = memberIds - cmd.clientUserId, + update = UpdateGroupShortNameChanged(groupId, newShortName) + ) + } yield seqState + + result pipeTo replyTo + } + } + } + } + } + + protected def addExt(cmd: AddExt): Unit = + cmd.ext match { + case Some(ext) ⇒ + persist(GroupEvents.ExtAdded(Instant.now, ext)) { evt ⇒ + val newState = commit(evt) + sendExtUpdate(newState) map (_ ⇒ AddExtAck()) pipeTo sender() + } + case None ⇒ + sender() ! Status.Failure(InvalidExtension) + } + + protected def removeExt(cmd: RemoveExt): Unit = + if (state.exts.exists(_.key == cmd.key)) { + persist(GroupEvents.ExtRemoved(Instant.now, cmd.key)) { evt ⇒ + val newState = commit(evt) + sendExtUpdate(newState) map (_ ⇒ RemoveExtAck()) pipeTo sender() + } + } else { + sender() ! RemoveExtAck() + } + + private def sendExtUpdate(state: GroupState): Future[Unit] = + seqUpdExt.broadcastPeopleUpdate( + userIds = state.memberIds, + update = UpdateGroupExtChanged(groupId, Some(extToApi(state.exts))) + ) + + private def getAvatarData(avatar: Option[Avatar]): AvatarData = + avatar + .map(ImageUtils.getAvatarData(AvatarData.OfGroup, groupId, _)) + .getOrElse(AvatarData.empty(AvatarData.OfGroup, groupId.toLong)) + + private def trimToEmpty(s: Option[String]): Option[String] = + s map (_.trim) filter (_.nonEmpty) + +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/group/MemberCommandHandlers.scala b/actor-server/actor-core/src/main/scala/im/actor/server/group/MemberCommandHandlers.scala new file mode 100644 index 0000000000..b3f261c18f --- /dev/null +++ b/actor-server/actor-core/src/main/scala/im/actor/server/group/MemberCommandHandlers.scala @@ -0,0 +1,737 @@ +package im.actor.server.group + +import java.time.{ Instant, LocalDateTime, ZoneOffset } + +import akka.actor.Status +import akka.http.scaladsl.util.FastFuture +import akka.pattern.pipe +import com.github.ghik.silencer.silent +import im.actor.api.rpc.Update +import im.actor.api.rpc.groups._ +import im.actor.api.rpc.messaging.{ ApiServiceMessage, UpdateChatDropCache, UpdateMessage } +import im.actor.concurrent.FutureExt +import im.actor.server.acl.ACLUtils +import im.actor.server.group.GroupCommands.{ Invite, Join, Kick, Leave } +import im.actor.server.group.GroupErrors.CantLeaveGroup +import im.actor.server.group.GroupEvents.{ UserInvited, UserJoined, UserKicked, UserLeft } +import im.actor.server.persist.{ GroupInviteTokenRepo, GroupUserRepo } +import im.actor.server.sequence.{ Optimization, SeqState, SeqStateDate } + +import scala.concurrent.Future + +private[group] trait MemberCommandHandlers extends GroupsImplicits { + this: GroupProcessor ⇒ + + import im.actor.server.ApiConversions._ + + protected def invite(cmd: Invite): Unit = { + if (!state.permissions.canInviteMembers(cmd.inviterUserId)) { + sender() ! noPermission + } else if (state.isInvited(cmd.inviteeUserId)) { + sender() ! Status.Failure(GroupErrors.UserAlreadyInvited) + } else if (state.isMember(cmd.inviteeUserId)) { + sender() ! Status.Failure(GroupErrors.UserAlreadyJoined) + } else { + val replyTo = sender() + + val isBlockedFu = checkIsBlocked(cmd.inviteeUserId, state.ownerUserId) + + onSuccess(isBlockedFu) { isBlocked ⇒ + if (isBlocked) { + replyTo ! Status.Failure(GroupErrors.UserIsBanned) + } else { + val inviteeIsExUser = state.isExUser(cmd.inviteeUserId) + + persist(UserInvited(Instant.now, cmd.inviteeUserId, cmd.inviterUserId)) { evt ⇒ + val newState = commit(evt) + + val dateMillis = evt.ts.toEpochMilli + val memberIds = newState.memberIds + + // TODO: unify isHistoryShared usage + val inviteeUpdatesNew: Vector[Update] = { + val optDrop = if (newState.isHistoryShared) Some(UpdateChatDropCache(apiGroupPeer)) else None + optDrop ++: refreshGroupUpdates(newState, cmd.inviteeUserId) + } + + // For groups with not async members we should push Diff for members, and all Members for invitee + // For groups with async members we should push UpdateGroupMembersCountChanged for both invitee and members + val (inviteeUpdateNew, membersUpdateNew): (Update, Update) = + if (newState.isAsyncMembers) { + val u = UpdateGroupMembersCountChanged(groupId, newState.membersCount) + (u, u) + } else { + val apiMembers = newState.members.values.map(_.asStruct).toVector + val inviteeMember = apiMembers.find(_.userId == cmd.inviteeUserId) + + ( + UpdateGroupMembersUpdated(groupId, apiMembers), + UpdateGroupMemberDiff( + groupId, + addedMembers = inviteeMember.toVector, + membersCount = newState.membersCount, + removedUsers = Vector.empty + ) + ) + } + + val inviteeUpdateObsolete = UpdateGroupInviteObsolete( + groupId, + inviteUserId = cmd.inviterUserId, + date = dateMillis, + randomId = cmd.randomId + ) + + val membersUpdateObsolete = UpdateGroupUserInvitedObsolete( + groupId, + userId = cmd.inviteeUserId, + inviterUserId = cmd.inviterUserId, + date = dateMillis, + randomId = cmd.randomId + ) + val serviceMessage = GroupServiceMessages.userInvited(cmd.inviteeUserId) + + //TODO: remove deprecated + db.run(GroupUserRepo.create(groupId, cmd.inviteeUserId, cmd.inviterUserId, evt.ts, None, isAdmin = false): @silent) + + def inviteGROUPUpdates: Future[SeqStateDate] = + for { + // push updated members list/count to inviteeUserId, + // make it `FatSeqUpdate` if this user invited to group for first time. + _ ← seqUpdExt.deliverUserUpdate( + userId = cmd.inviteeUserId, + update = inviteeUpdateNew, + pushRules = seqUpdExt.pushRules(isFat = !inviteeIsExUser, Some(PushTexts.invited(newState.groupType))), + deliveryId = s"invite_${groupId}_${cmd.randomId}" + ) + + // push all "refresh group" updates to inviteeUserId + _ ← FutureExt.ftraverse(inviteeUpdatesNew) { update ⇒ + seqUpdExt.deliverUserUpdate(userId = cmd.inviteeUserId, update) + } + + // push updated members difference to all group members except inviteeUserId + SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( + userId = cmd.inviterUserId, + authId = cmd.inviterAuthId, + bcastUserIds = (memberIds - cmd.inviterUserId) - cmd.inviteeUserId, + update = membersUpdateNew, + deliveryId = s"useradded_${groupId}_${cmd.randomId}" + ) + + // explicitly send service message + SeqStateDate(_, _, date) ← dialogExt.sendServerMessage( + apiGroupPeer, + cmd.inviterUserId, + cmd.inviterAuthId, + cmd.randomId, + serviceMessage, + deliveryTag = Some(Optimization.GroupV2) + ) + } yield SeqStateDate(seq, state, date) + + def inviteCHANNELUpdates: Future[SeqStateDate] = + for { + // push updated members count to inviteeUserId + _ ← seqUpdExt.deliverUserUpdate( + userId = cmd.inviteeUserId, + update = inviteeUpdateNew, + pushRules = seqUpdExt.pushRules(isFat = false, Some(PushTexts.invited(newState.groupType))), + deliveryId = s"invite_${groupId}_${cmd.randomId}" + ) + + // push all "refresh group" updates to inviteeUserId + _ ← FutureExt.ftraverse(inviteeUpdatesNew) { update ⇒ + seqUpdExt.deliverUserUpdate(userId = cmd.inviteeUserId, update) + } + + // push updated members count to all group members + SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( + userId = cmd.inviterUserId, + authId = cmd.inviterAuthId, + bcastUserIds = (memberIds - cmd.inviterUserId) - cmd.inviteeUserId, + update = membersUpdateNew, + deliveryId = s"useradded_${groupId}_${cmd.randomId}" + ) + + // push service message to invitee + _ ← seqUpdExt.deliverUserUpdate( + userId = cmd.inviteeUserId, + update = serviceMessageUpdate( + cmd.inviterUserId, + dateMillis, + cmd.randomId, + serviceMessage + ), + deliveryTag = Some(Optimization.GroupV2) + ) + _ ← dialogExt.bump(cmd.inviteeUserId, apiGroupPeer.asModel) + } yield SeqStateDate(seq, state, dateMillis) + + val result: Future[SeqStateDate] = for { + /////////////////////////// + // Groups V1 API updates // + /////////////////////////// + + // push "Invited" to invitee + _ ← seqUpdExt.deliverUserUpdate( + userId = cmd.inviteeUserId, + inviteeUpdateObsolete, + pushRules = seqUpdExt.pushRules(isFat = true, Some(PushTexts.invited(newState.groupType))), + deliveryId = s"invite_obsolete_${groupId}_${cmd.randomId}" + ) + + // push "User added" to all group members except for `inviterUserId` + _ ← seqUpdExt.broadcastPeopleUpdate( + (memberIds - cmd.inviteeUserId) - cmd.inviterUserId, // is it right? + membersUpdateObsolete, + pushRules = seqUpdExt.pushRules(isFat = true, Some(PushTexts.Added)), + deliveryId = s"useradded_obsolete_${groupId}_${cmd.randomId}" + ) + + // push "User added" to `inviterUserId` + _ ← seqUpdExt.deliverClientUpdate( + cmd.inviterUserId, + cmd.inviterAuthId, + membersUpdateObsolete, + pushRules = seqUpdExt.pushRules(isFat = true, None), + deliveryId = s"useradded_obsolete_${groupId}_${cmd.randomId}" + ) + + /////////////////////////// + // Groups V2 API updates // + /////////////////////////// + + seqStateDate ← if (newState.groupType.isChannel) inviteCHANNELUpdates else inviteGROUPUpdates + + } yield seqStateDate + + result pipeTo replyTo + } + } + } + } + } + + /** + * User can join + * • after invite(was invited by other user previously). In this case he already have group on devices + * • via invite link. In this case he doesn't have group, and we need to deliver it. + */ + protected def join(cmd: Join): Unit = { + // user is already a member, and should not complete invitation process + if (state.isMember(cmd.joiningUserId) && !state.isInvited(cmd.joiningUserId)) { + sender() ! Status.Failure(GroupErrors.UserAlreadyJoined) + } else { + val replyTo = sender() + + val isBlockedFu = checkIsBlocked(cmd.joiningUserId, state.ownerUserId) + + onSuccess(isBlockedFu) { isBlocked ⇒ + if (isBlocked) { + replyTo ! Status.Failure(GroupErrors.UserIsBanned) + } else { + // user was invited in group by other group user + val wasInvited = state.isInvited(cmd.joiningUserId) + + // trying to figure out who invited joining user. + // Descending priority: + // • inviter defined in `Join` command (when invited via token) + // • inviter from members list (when invited by other user) + // • group creator (safe fallback) + val optMember = state.members.get(cmd.joiningUserId) + val inviterUserId = cmd.invitingUserId + .orElse(optMember.map(_.inviterUserId)) + .getOrElse(state.ownerUserId) + + persist(UserJoined(Instant.now, cmd.joiningUserId, inviterUserId)) { evt ⇒ + val newState = commit(evt) + + val date = evt.ts + val dateMillis = date.toEpochMilli + val showJoinMessage = newState.adminSettings.showJoinLeaveMessages + val memberIds = newState.memberIds + val apiMembers = newState.members.values.map(_.asStruct).toVector + val randomId = ACLUtils.randomLong() + + // If user was never invited to group - he don't have group on devices, + // that means we need to push all group-info related updates + // + // If user was invited to group by other member - we don't need to push group updates, + // cause they were pushed already on invite step + // TODO: unify isHistoryShared usage + val joiningUserUpdatesNew: Vector[Update] = { + if (wasInvited) { + Vector.empty[Update] + } else { + val optDrop = if (newState.isHistoryShared) Some(UpdateChatDropCache(apiGroupPeer)) else None + optDrop ++: refreshGroupUpdates(newState, cmd.joiningUserId) + } + } + + // For groups with not async members we should push: + // • Diff for members; + // • Diff for joining user if he was previously invited; + // • Members for joining user if he wasn't previously invited. + // + // For groups with async members we should push: + // • UpdateGroupMembersCountChanged for both joining user and members + val (joiningUpdateNew, membersUpdateNew): (Update, Update) = + if (newState.isAsyncMembers) { + val u = UpdateGroupMembersCountChanged(groupId, newState.membersCount) + (u, u) + } else { + val joiningMember = apiMembers.find(_.userId == cmd.joiningUserId) + val diff = UpdateGroupMemberDiff( + groupId, + addedMembers = joiningMember.toVector, + membersCount = newState.membersCount, + removedUsers = Vector.empty + ) + + if (wasInvited) { + (diff, diff) + } else { + ( + UpdateGroupMembersUpdated(groupId, apiMembers), + diff + ) + } + } + + // TODO: not sure how it should be in old API + val membersUpdateObsolete = UpdateGroupMembersUpdateObsolete(groupId, apiMembers) + + val serviceMessage = GroupServiceMessages.userJoined + + //TODO: remove deprecated + db.run(GroupUserRepo.create( + groupId, + userId = cmd.joiningUserId, + inviterUserId = inviterUserId, + invitedAt = optMember.map(_.invitedAt).getOrElse(date), + joinedAt = Some(LocalDateTime.now(ZoneOffset.UTC)), + isAdmin = false + ): @silent) + + def joinGROUPUpdates: Future[SeqStateDate] = + for { + // push all group updates to joiningUserId + _ ← FutureExt.ftraverse(joiningUserUpdatesNew) { update ⇒ + seqUpdExt.deliverUserUpdate(userId = cmd.joiningUserId, update) + } + + // push updated members list/count/difference to joining user, + // make it `FatSeqUpdate` if this user invited to group for first time. + // TODO???: isFat = !wasInvited - is it correct? + SeqState(seq, state) ← seqUpdExt.deliverClientUpdate( + userId = cmd.joiningUserId, + authId = cmd.joiningUserAuthId, + update = joiningUpdateNew, + pushRules = seqUpdExt.pushRules(isFat = !wasInvited, None), //!wasInvited means that user came for first time here + deliveryId = s"join_${groupId}_${randomId}" + + ) + + // push updated members list/count to all group members except joiningUserId + _ ← seqUpdExt.broadcastPeopleUpdate( + memberIds - cmd.joiningUserId, + membersUpdateNew, + deliveryId = s"userjoined_${groupId}_${randomId}" + ) + + date ← if (showJoinMessage) { + dialogExt.sendServerMessage( + apiGroupPeer, + senderUserId = cmd.joiningUserId, + senderAuthId = cmd.joiningUserAuthId, + randomId = randomId, + serviceMessage // no delivery tag. This updated handled this way in Groups V1 + ) map (_.date) + } else { + // write service message only for joining user + // and push join message + for { + _ ← dialogExt.writeMessageSelf( + userId = cmd.joiningUserId, + peer = apiGroupPeer, + senderUserId = cmd.joiningUserId, + dateMillis = dateMillis, + randomId = randomId, + serviceMessage + ) + _ ← seqUpdExt.deliverUserUpdate( + userId = cmd.joiningUserId, + update = serviceMessageUpdate( + cmd.joiningUserId, + dateMillis, + randomId, + serviceMessage + ), + deliveryTag = Some(Optimization.GroupV2) + ) + } yield dateMillis + } + } yield SeqStateDate(seq, state, date) + + def joinCHANNELUpdates: Future[SeqStateDate] = + for { + // push all group updates to joiningUserId + _ ← FutureExt.ftraverse(joiningUserUpdatesNew) { update ⇒ + seqUpdExt.deliverUserUpdate(userId = cmd.joiningUserId, update) + } + + // push updated members count to joining user + SeqState(seq, state) ← seqUpdExt.deliverClientUpdate( + userId = cmd.joiningUserId, + authId = cmd.joiningUserAuthId, + update = joiningUpdateNew, + deliveryId = s"join_${groupId}_${randomId}" + ) + + // push updated members count to all group members except joining user + _ ← seqUpdExt.broadcastPeopleUpdate( + memberIds - cmd.joiningUserId, + membersUpdateNew, + deliveryId = s"userjoined_${groupId}_${randomId}" + ) + + // push join message only to joining user + _ ← seqUpdExt.deliverUserUpdate( + userId = cmd.joiningUserId, + update = serviceMessageUpdate( + cmd.joiningUserId, + dateMillis, + randomId, + serviceMessage + ), + deliveryTag = Some(Optimization.GroupV2) + ) + _ ← dialogExt.bump(cmd.joiningUserId, apiGroupPeer.asModel) + } yield SeqStateDate(seq, state, dateMillis) + + val result: Future[(SeqStateDate, Vector[Int], Long)] = + for { + /////////////////////////// + // Groups V1 API updates // + /////////////////////////// + + // push update about members to all users, except joining user + _ ← seqUpdExt.broadcastPeopleUpdate( + memberIds - cmd.joiningUserId, + membersUpdateObsolete, + pushRules = seqUpdExt.pushRules(isFat = true, None), + deliveryId = s"userjoined_obsolete_${groupId}_${randomId}" + ) + + /////////////////////////// + // Groups V2 API updates // + /////////////////////////// + + seqStateDate ← if (newState.groupType.isChannel) joinCHANNELUpdates else joinGROUPUpdates + + } yield (seqStateDate, memberIds.toVector :+ inviterUserId, randomId) + + result pipeTo replyTo + } + } + } + } + } + + /** + * This case handled in other manner, so we change state in the end + * cause user that left, should send service message. And we don't allow non-members + * to send message. So we keep him as member until message sent, and remove him from members + */ + protected def leave(cmd: Leave): Unit = { + if (state.nonMember(cmd.userId)) { + sender() ! notMember + } else if (!state.permissions.canLeave(cmd.userId)) { + sender() ! Status.Failure(CantLeaveGroup) + } else { + val leftEvent = UserLeft(Instant.now, cmd.userId) + persist(leftEvent) { evt ⇒ + // no commit here. it will be after service message sent + + val dateMillis = evt.ts.toEpochMilli + val showLeaveMessage = state.adminSettings.showJoinLeaveMessages + + val updateObsolete = UpdateGroupUserLeaveObsolete(groupId, cmd.userId, dateMillis, cmd.randomId) + + val updatePermissions = permissionsUpdates(cmd.userId, currState = state.updated(leftEvent)) + + val membersUpdateNew = + if (state.isAsyncMembers) { + UpdateGroupMembersCountChanged( + groupId, + membersCount = state.membersCount - 1 + ) + } else { + UpdateGroupMemberDiff( + groupId, + removedUsers = Vector(cmd.userId), + addedMembers = Vector.empty, + membersCount = state.membersCount - 1 + ) + } + + val serviceMessage = GroupServiceMessages.userLeft + + //TODO: remove deprecated. GroupInviteTokenRepo don't have replacement yet. + db.run( + for { + _ ← GroupUserRepo.delete(groupId, cmd.userId): @silent + _ ← GroupInviteTokenRepo.revoke(groupId, cmd.userId): @silent + } yield () + ) + + val leaveGROUPUpdates: Future[SeqStateDate] = + for { + // push updated members list to all group members + _ ← seqUpdExt.broadcastPeopleUpdate( + state.memberIds - cmd.userId, + membersUpdateNew + ) + + // send service message + date ← if (showLeaveMessage) { + dialogExt.sendServerMessage( + apiGroupPeer, + senderUserId = cmd.userId, + senderAuthId = cmd.authId, + randomId = cmd.randomId, + message = serviceMessage, + deliveryTag = Some(Optimization.GroupV2) + ) map (_.date) + } else { + FastFuture.successful(dateMillis) + } + + // push left user that he is no longer a member + SeqState(seq, state) ← seqUpdExt.deliverClientUpdate( + userId = cmd.userId, + authId = cmd.authId, + update = UpdateGroupMemberChanged(groupId, isMember = false) + ) + + // push left user updates + // • with empty group members + // • that he can't view and invite members + leftUpdates = updatePermissions :+ UpdateGroupMembersUpdated(groupId, members = Vector.empty) + _ ← FutureExt.ftraverse(leftUpdates) { update ⇒ + seqUpdExt.deliverUserUpdate(userId = cmd.userId, update) + } + } yield SeqStateDate(seq, state, date) + + val leaveCHANNELUpdates: Future[SeqStateDate] = + for { + // push updated members count to all group members + _ ← seqUpdExt.broadcastPeopleUpdate( + state.memberIds - cmd.userId, + membersUpdateNew + ) + + // push left user that he is no longer a member + SeqState(seq, state) ← seqUpdExt.deliverClientUpdate( + userId = cmd.userId, + authId = cmd.authId, + update = UpdateGroupMemberChanged(groupId, isMember = false) + ) + + // push left user updates that he has no group rights + leftUpdates = updatePermissions :+ UpdateGroupMembersCountChanged(groupId, membersCount = 0) + _ ← FutureExt.ftraverse(leftUpdates) { update ⇒ + seqUpdExt.deliverUserUpdate(userId = cmd.userId, update) + } + } yield SeqStateDate(seq, state, dateMillis) + + // read this dialog by user that leaves group. don't wait for ack + dialogExt.messageRead(apiGroupPeer, cmd.userId, 0L, dateMillis) + val result: Future[SeqStateDate] = for { + + /////////////////////////// + // Groups V1 API updates // + /////////////////////////// + + _ ← seqUpdExt.broadcastClientUpdate( + userId = cmd.userId, + authId = cmd.authId, + bcastUserIds = state.memberIds + cmd.userId, // push this to other user's devices too. actually cmd.userId is still in state.memberIds + update = updateObsolete, + pushRules = seqUpdExt.pushRules(isFat = false, Some(PushTexts.Left), Seq(cmd.authId)) + ) + + /////////////////////////// + // Groups V2 API updates // + /////////////////////////// + + seqStateDate ← if (state.groupType.isChannel) leaveCHANNELUpdates else leaveGROUPUpdates + + } yield seqStateDate + + result andThen { case _ ⇒ commit(evt) } pipeTo sender() + } + } + } + + protected def kick(cmd: Kick): Unit = { + val canKick = + state.permissions.canKickAnyone(cmd.kickerUserId) || + ( + state.permissions.canKickInvited(cmd.kickerUserId) && + state.members.get(cmd.kickedUserId).exists(_.inviterUserId == cmd.kickerUserId) // user we kick invited by kicker + ) + if (!canKick) { + sender() ! noPermission + } else if (state.nonMember(cmd.kickedUserId)) { + sender() ! notMember + } else { + persist(UserKicked(Instant.now, cmd.kickedUserId, cmd.kickerUserId)) { evt ⇒ + val newState = commit(evt) + + val dateMillis = evt.ts.toEpochMilli + + val updateObsolete = UpdateGroupUserKickObsolete(groupId, cmd.kickedUserId, cmd.kickerUserId, dateMillis, cmd.randomId) + + val updatePermissions = permissionsUpdates(cmd.kickedUserId, newState) + + val membersUpdateNew: Update = + if (newState.isAsyncMembers) { + UpdateGroupMembersCountChanged( + groupId, + membersCount = newState.membersCount + ) + } else { + UpdateGroupMemberDiff( + groupId, + removedUsers = Vector(cmd.kickedUserId), + addedMembers = Vector.empty, + membersCount = newState.membersCount + ) + } + + val serviceMessage = GroupServiceMessages.userKicked(cmd.kickedUserId) + + //TODO: remove deprecated. GroupInviteTokenRepo don't have replacement yet. + db.run( + for { + _ ← GroupUserRepo.delete(groupId, cmd.kickedUserId): @silent + _ ← GroupInviteTokenRepo.revoke(groupId, cmd.kickedUserId): @silent + } yield () + ) + + val kickGROUPUpdates: Future[SeqStateDate] = + for { + // push updated members list to all group members. Don't push to kicked user! + SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( + userId = cmd.kickerUserId, + authId = cmd.kickerAuthId, + bcastUserIds = newState.memberIds - cmd.kickerUserId, + update = membersUpdateNew + ) + + SeqStateDate(_, _, date) ← dialogExt.sendServerMessage( + apiGroupPeer, + senderUserId = cmd.kickerUserId, + senderAuthId = cmd.kickerAuthId, + randomId = cmd.randomId, + message = serviceMessage, + deliveryTag = Some(Optimization.GroupV2) + ) + + // push kicked user updates + // • with empty group members + // • that he is no longer a member of group + // • that he can't view and invite members + kickedUserUpdates = updatePermissions ++ Vector( + UpdateGroupMembersUpdated(groupId, members = Vector.empty), + UpdateGroupMemberChanged(groupId, isMember = false) + ) + _ ← FutureExt.ftraverse(kickedUserUpdates) { update ⇒ + seqUpdExt.deliverUserUpdate(userId = cmd.kickedUserId, update) + } + } yield SeqStateDate(seq, state, date) + + val kickCHANNELUpdates: Future[SeqStateDate] = + for { + // push updated members count to all group members. Don't push to kicked user! + SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( + userId = cmd.kickerUserId, + authId = cmd.kickerAuthId, + bcastUserIds = newState.memberIds - cmd.kickerUserId, + update = membersUpdateNew + ) + + // push service message to kicker and kicked users. + _ ← seqUpdExt.broadcastPeopleUpdate( + userIds = Set(cmd.kickedUserId, cmd.kickerUserId), + update = serviceMessageUpdate( + cmd.kickerUserId, + dateMillis, + cmd.randomId, + serviceMessage + ), + deliveryTag = Some(Optimization.GroupV2) + ) + + // push kicked user updates that he has no group rights + kickedUserUpdates = updatePermissions :+ UpdateGroupMemberChanged(groupId, isMember = false) + _ ← FutureExt.ftraverse(kickedUserUpdates) { update ⇒ + seqUpdExt.deliverUserUpdate(userId = cmd.kickedUserId, update) + } + } yield SeqStateDate(seq, state, dateMillis) + + // read this dialog by kicked user. don't wait for ack + dialogExt.messageRead(apiGroupPeer, cmd.kickedUserId, 0L, dateMillis) + val result: Future[SeqStateDate] = for { + /////////////////////////// + // Groups V1 API updates // + /////////////////////////// + + _ ← seqUpdExt.broadcastClientUpdate( + userId = cmd.kickerUserId, + authId = cmd.kickerAuthId, + bcastUserIds = newState.memberIds, + update = updateObsolete, + pushRules = seqUpdExt.pushRules(isFat = false, Some(PushTexts.Kicked), Seq(cmd.kickerAuthId)) + ) + + /////////////////////////// + // Groups V2 API updates // + /////////////////////////// + + seqStateDate ← if (state.groupType.isChannel) kickCHANNELUpdates else kickGROUPUpdates + + } yield seqStateDate + + result pipeTo sender() + } + } + } + + // Updates that will be sent to user, when he enters group. + // Helps clients that have this group to refresh it's data. + private def refreshGroupUpdates(newState: GroupState, userId: Int): Vector[Update] = Vector( + UpdateGroupMemberChanged(groupId, isMember = true), + UpdateGroupAboutChanged(groupId, newState.about), + UpdateGroupAvatarChanged(groupId, newState.avatar), + UpdateGroupTopicChanged(groupId, newState.topic), + UpdateGroupTitleChanged(groupId, newState.title), + UpdateGroupOwnerChanged(groupId, newState.ownerUserId) + // UpdateGroupExtChanged(groupId, newState.extension) //TODO: figure out and fix + // if(bigGroup) UpdateGroupMembersCountChanged(groupId, newState.extension) + ) ++ permissionsUpdates(userId, newState) + + private def serviceMessageUpdate(senderUserId: Int, date: Long, randomId: Long, message: ApiServiceMessage) = + UpdateMessage( + peer = apiGroupPeer, + senderUserId = senderUserId, + date = date, + randomId = randomId, + message = message, + attributes = None, + quotedMessage = None + ) + +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/group/PushTexts.scala b/actor-server/actor-core/src/main/scala/im/actor/server/group/PushTexts.scala new file mode 100644 index 0000000000..bdb45dc401 --- /dev/null +++ b/actor-server/actor-core/src/main/scala/im/actor/server/group/PushTexts.scala @@ -0,0 +1,29 @@ +package im.actor.server.group + +//TODO: make up to date with channels +object PushTexts { + val Added = "User added" + val Kicked = "User kicked" + val Left = "User left" + + def invited(gt: GroupType) = + if (gt.isChannel) { + "You are invited to a channel" + } else { + "You are invited to a group" + } + + def titleChanged(gt: GroupType) = + if (gt.isChannel) { + "Channel title changed" + } else { + "Group title changed" + } + + def topicChanged(gt: GroupType) = + if (gt.isChannel) { + "Channel topic changed" + } else { + "Group topic changed" + } +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/group/http/GroupsHttpHandler.scala b/actor-server/actor-core/src/main/scala/im/actor/server/group/http/GroupsHttpHandler.scala index 0e64b70c2a..13d56db3fa 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/group/http/GroupsHttpHandler.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/group/http/GroupsHttpHandler.scala @@ -6,36 +6,34 @@ import akka.http.scaladsl.model.StatusCodes._ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import akka.http.scaladsl.util.FastFuture -import im.actor.server.api.http.{ HttpHandler, json } +import com.github.ghik.silencer.silent import im.actor.server.api.http.json.JsonFormatters.{ errorsFormat, groupInviteInfoFormat } +import im.actor.server.api.http.{ HttpHandler, json } import im.actor.server.db.DbExtension -import im.actor.server.file.ImageUtils.getAvatar import im.actor.server.file.{ Avatar, FileLocation, FileStorageExtension } import im.actor.server.group.GroupExtension -import im.actor.server.model.AvatarData +import im.actor.server.names.GlobalNamesStorageKeyValueStorage import im.actor.server.persist._ import im.actor.server.persist.files.FileRepo import im.actor.server.user.UserExtension import play.api.libs.json.Json -import slick.driver.PostgresDriver.api._ import scala.concurrent.Future -import scala.concurrent.duration._ import scala.util.{ Failure, Success } private[group] final class GroupsHttpHandler()(implicit system: ActorSystem) extends HttpHandler { import im.actor.server.ApiConversions._ - import system.dispatcher private val db = DbExtension(system).db private val fsAdapter = FileStorageExtension(system).fsAdapter + private val globalNamesStorage = new GlobalNamesStorageKeyValueStorage override def routes: Route = defaultVersion { - path("groups" / "invites" / Segment) { token ⇒ + path("groups" / "invites" / Segment) { tokenOrShortName ⇒ get { - onComplete(retrieve(token)) { + onComplete(retrieve(tokenOrShortName)) { case Success(Right(result)) ⇒ complete(HttpResponse( status = OK, @@ -52,25 +50,36 @@ private[group] final class GroupsHttpHandler()(implicit system: ActorSystem) ext } } - private def retrieve(token: String): Future[Either[json.Errors, json.GroupInviteInfo]] = for { - optTokenInfo ← db.run(GroupInviteTokenRepo.findByToken(token)) - result ← optTokenInfo map { tokenInfo ⇒ - for { - groupInfo ← GroupExtension(system).getApiStruct(tokenInfo.groupId, 0) - groupTitle = groupInfo.title - groupAvatar = groupInfo.avatar - groupAvatarUrls ← avatarUrls(groupAvatar) + private def retrieve(tokenOrShortName: String): Future[Either[json.Errors, json.GroupInviteInfo]] = for { + byToken ← db.run(GroupInviteTokenRepo.findByToken(tokenOrShortName): @silent) + byGroupId ← globalNamesStorage.getGroupId(tokenOrShortName) + optInviteData = (byToken, byGroupId) match { + case (Some(tokenInfo), _) ⇒ Some(tokenInfo.groupId → Some(tokenInfo.creatorId)) + case (_, Some(groupId)) ⇒ Some(groupId → None) + case _ ⇒ None + } + result ← optInviteData map { + case (groupId, optInviterId) ⇒ + for { + groupInfo ← GroupExtension(system).getApiStruct(groupId, 0) + isPublic ← GroupExtension(system).getApiFullStruct(groupId, 0) map (_.shortName.isDefined) + groupTitle = groupInfo.title + groupAvatarUrls ← avatarUrls(groupInfo.avatar) - inviterInfo ← UserExtension(system).getApiStruct(tokenInfo.creatorId, 0, 0L) - inviterName = inviterInfo.name - inviterAvatar = inviterInfo.avatar - inviterAvatarUrls ← avatarUrls(inviterAvatar) - } yield Right( - json.GroupInviteInfo( - group = json.Group(groupTitle, groupAvatarUrls), - inviter = json.User(inviterName, inviterAvatarUrls) + optInviterInfo ← optInviterId match { + case Some(inviterId) ⇒ + for { + user ← UserExtension(system).getApiStruct(inviterId, 0, 0L) + avatars ← avatarUrls(user.avatar) + } yield Some(json.InviterInfo(user.name, avatars)) + case None ⇒ FastFuture.successful(None) + } + } yield Right( + json.GroupInviteInfo( + group = json.GroupInfo(groupId, groupTitle, isPublic, groupAvatarUrls), + inviter = optInviterInfo + ) ) - ) } getOrElse FastFuture.successful(Left(json.Errors("Expired or invalid token"))) } yield result diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/messaging/MessageUpdating.scala b/actor-server/actor-core/src/main/scala/im/actor/server/messaging/MessageUpdating.scala index 84ec97e2f7..620affd8d1 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/messaging/MessageUpdating.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/messaging/MessageUpdating.scala @@ -5,6 +5,7 @@ import im.actor.api.rpc.PeersImplicits import im.actor.api.rpc.messaging.{ ApiMessage, UpdateMessageContentChanged } import im.actor.api.rpc.peers.{ ApiPeer, ApiPeerType } import im.actor.server.db.DbExtension +import im.actor.server.dialog.HistoryUtils import im.actor.server.group.GroupExtension import im.actor.server.model.{ Peer, PeerType } import im.actor.server.persist.HistoryMessageRepo @@ -51,10 +52,17 @@ trait MessageUpdating extends PeersImplicits { } yield seqState } - private def updateContentGroup(userId: Int, clientAuthId: Long, peer: Peer, randomId: Long, updatedMessage: ApiMessage, date: Long)(implicit system: ActorSystem): Future[SeqState] = { + private def updateContentGroup( + userId: Int, + clientAuthId: Long, + groupPeer: Peer, + randomId: Long, + updatedMessage: ApiMessage, + date: Long + )(implicit system: ActorSystem): Future[SeqState] = { import system.dispatcher val seqUpdExt = SeqUpdatesExtension(system) - val update = UpdateMessageContentChanged(peer.asStruct, randomId, updatedMessage) + val update = UpdateMessageContentChanged(groupPeer.asStruct, randomId, updatedMessage) for { // update for client user seqState ← seqUpdExt.deliverClientUpdate( @@ -64,8 +72,9 @@ trait MessageUpdating extends PeersImplicits { pushRules = seqUpdExt.pushRules(isFat = false, None), deliveryId = s"msgcontent_${randomId}_${date}" ) - (memberIds, _, _) ← GroupExtension(system).getMemberIds(peer.id) - membersSet = memberIds.toSet + (memberIds, _, optBotId) ← GroupExtension(system).getMemberIds(groupPeer.id) + isShared ← GroupExtension(system).isHistoryShared(groupPeer.id) + membersSet = (memberIds ++ optBotId.toSeq).toSet // update for other group members _ ← seqUpdExt.broadcastPeopleUpdate( membersSet - userId, @@ -73,14 +82,25 @@ trait MessageUpdating extends PeersImplicits { pushRules = seqUpdExt.pushRules(isFat = false, None), deliveryId = s"msgcontent_${randomId}_${date}" ) - _ ← DbExtension(system).db.run(HistoryMessageRepo.updateContentAll( - userIds = membersSet + userId, - randomId = randomId, - peerType = PeerType.Group, - peerIds = Set(peer.id), - messageContentHeader = updatedMessage.header, - messageContentData = updatedMessage.toByteArray - )) + _ ← if (isShared) { + DbExtension(system).db.run(HistoryMessageRepo.updateContentAll( + userIds = Set(HistoryUtils.SharedUserId), + randomId = randomId, + peerType = PeerType.Group, + peerIds = Set(groupPeer.id), + messageContentHeader = updatedMessage.header, + messageContentData = updatedMessage.toByteArray + )) + } else { + DbExtension(system).db.run(HistoryMessageRepo.updateContentAll( + userIds = membersSet + userId, + randomId = randomId, + peerType = PeerType.Group, + peerIds = Set(groupPeer.id), + messageContentHeader = updatedMessage.header, + messageContentData = updatedMessage.toByteArray + )) + } } yield seqState } diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/migrations/HiddenGroupMigrator.scala b/actor-server/actor-core/src/main/scala/im/actor/server/migrations/HiddenGroupMigrator.scala index 1ee868947e..37bb78b9aa 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/migrations/HiddenGroupMigrator.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/migrations/HiddenGroupMigrator.scala @@ -2,6 +2,7 @@ package im.actor.server.migrations import akka.actor.{ ActorLogging, ActorSystem, PoisonPill, Props } import akka.persistence.{ PersistentActor, RecoveryCompleted } +import com.github.ghik.silencer.silent import im.actor.concurrent.FutureExt import im.actor.server.db.DbExtension import im.actor.server.event.TSEvent @@ -52,7 +53,7 @@ private final class HiddenGroupMigrator(promise: Promise[Unit], groupId: Int) ex private def migrate(): Unit = { if (isHidden) { - db.run(GroupRepo.makeHidden(groupId)) onComplete { + db.run(GroupRepo.makeHidden(groupId): @silent) onComplete { case Failure(e) ⇒ promise.failure(e) self ! PoisonPill diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/names/GlobalNamesStorage.scala b/actor-server/actor-core/src/main/scala/im/actor/server/names/GlobalNamesStorage.scala new file mode 100644 index 0000000000..207af1e126 --- /dev/null +++ b/actor-server/actor-core/src/main/scala/im/actor/server/names/GlobalNamesStorage.scala @@ -0,0 +1,149 @@ +package im.actor.server.names + +import akka.actor.ActorSystem +import akka.http.scaladsl.util.FastFuture +import com.github.ghik.silencer.silent +import im.actor.server.db.DbExtension +import im.actor.server.persist.UserRepo +import im.actor.storage.SimpleStorage +import slick.dbio._ + +import scala.concurrent.Future + +/** + * Stores mapping "Normalized global name" -> "Global name owner(group/user)" + * normalized global name: String + * global name owner: im.actor.server.names.GlobalNameOwner + */ +private object GlobalNamesStorage extends SimpleStorage("global_names") + +/** + * Storage that keeps compatibility between + * storing nicknames in `im.actor.server.persist.UserRepo` + * and storing group and user names in new `im.actor.storage.SimpleStorage` storage + */ +final class GlobalNamesStorageKeyValueStorage(implicit system: ActorSystem) { + import system.dispatcher + + private val (db, conn) = { + val ext = DbExtension(system) + (ext.db, ext.connector) + } + + def getUserId(name: String): Future[Option[Int]] = + getOwner(name) map (_.collect { + case GlobalNameOwner(OwnerType.User, userId) ⇒ userId + }) + + /** + * Search groups(id -> global name) by global name prefix + * Looks only in GlobalNamesStorage + */ + def groupIdsByPrefix(namePrefix: String): Future[IndexedSeq[(Int, String)]] = { + conn.run(GlobalNamesStorage.getByPrefix(normalized(namePrefix))) map { searchResults ⇒ + searchResults flatMap { + case (fullName, bytes) ⇒ + Some(GlobalNameOwner.parseFrom(bytes)) filter (_.ownerType.isGroup) map (o ⇒ o.ownerId → fullName) + } + } + } + + /** + * Search users(id -> global name) by global name prefix + * Looks in both GlobalNamesStorage and UserRepo(compatibility mode) + */ + def userIdsByPrefix(namePrefix: String): Future[IndexedSeq[(Int, String)]] = { + val kvSearch = conn.run(GlobalNamesStorage.getByPrefix(normalized(namePrefix))) map { searchResults ⇒ + searchResults flatMap { + case (fullName, bytes) ⇒ + Some(GlobalNameOwner.parseFrom(bytes)) filter (_.ownerType.isUser) map (o ⇒ o.ownerId → fullName) + } + } + val compatSearch = db.run(UserRepo.findByNicknamePrefix(namePrefix): @silent) map { users ⇒ + users flatMap { user ⇒ + user.nickname map (user.id → _) + } + } + for { + kv ← kvSearch + compat ← compatSearch + } yield kv ++ compat + } + + def getGroupId(name: String): Future[Option[Int]] = + getOwner(name) map (_.collect { + case GlobalNameOwner(OwnerType.Group, groupId) ⇒ groupId + }) + + /** + * Compatible with storing nicknames in `im.actor.server.persist.UserRepo` + */ + def exists(name: String): Future[Boolean] = { + val existsInKV = conn.run(GlobalNamesStorage.get(normalized(name))) map (_.isDefined) + + existsInKV flatMap { + case true ⇒ FastFuture.successful(true) + case false ⇒ db.run(UserRepo.nicknameExists(name): @silent) + } + } + + /** + * `oldGlobalName` = None, `newGlobalName` = Some("name") - insert new name + * `oldGlobalName` = Some("oldName"), `newGlobalName` = Some("name") - update existing name + * `oldGlobalName` = Some("oldName"), `newGlobalName` = None - delete existing name + * `oldGlobalName` = None, `newGlobalName` = None - does nothing + */ + def updateOrRemove(oldGlobalName: Option[String], newGlobalName: Option[String], owner: GlobalNameOwner): Future[Unit] = { + val deleteFu = (oldGlobalName map delete) getOrElse FastFuture.successful(()) + val upsertFu = (newGlobalName map (n ⇒ upsert(n, owner))) getOrElse FastFuture.successful(()) + for { + _ ← deleteFu + _ ← upsertFu + } yield () + } + + /** + * Compatible with storing nicknames in `im.actor.server.persist.UserRepo` + */ + private def getOwner(name: String): Future[Option[GlobalNameOwner]] = { + val optOwner = conn.run( + GlobalNamesStorage.get(normalized(name)) + ) map { _ map GlobalNameOwner.parseFrom } + + optOwner flatMap { + case o @ Some(_) ⇒ FastFuture.successful(o) + case None ⇒ db.run(UserRepo.findByNickname(name): @silent) map (_.map(u ⇒ GlobalNameOwner(OwnerType.User, u.id))) + } + } + + private def upsert(name: String, owner: GlobalNameOwner): Future[Unit] = + conn.run( + GlobalNamesStorage.upsert(normalized(name), owner.toByteArray) + ) map (_ ⇒ ()) + + /** + * Compatible with storing nicknames in `im.actor.server.persist.UserRepo` + */ + private def delete(name: String): Future[Unit] = { + val kvDelete = conn.run(GlobalNamesStorage.delete(normalized(name))) + + kvDelete flatMap { count ⇒ + if (count == 0) { + db.run { + for { + optUser ← UserRepo.findByNickname(name): @silent + _ ← optUser match { + case Some(u) ⇒ UserRepo.setNickname(u.id, None): @silent + case None ⇒ DBIO.successful(0) + } + } yield () + } + } else { + FastFuture.successful(()) + } + } + } + + private def normalized(name: String) = name.toLowerCase + +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/office/Processor.scala b/actor-server/actor-core/src/main/scala/im/actor/server/office/Processor.scala index cfcbfd7625..3988b2e8b8 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/office/Processor.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/office/Processor.scala @@ -22,7 +22,7 @@ case object StopOffice trait ProcessorState -@deprecated("use im.actor.server.cqrs.Processor instead", "2016-07-07") +// TODO: replace with im.actor.server.cqrs.Processor trait Processor[State, Event <: AnyRef] extends PersistentActor with ActorFutures with AlertingActor { case class BreakStashing(ts: Instant, evts: Seq[Event], state: State) diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/office/PushTexts.scala b/actor-server/actor-core/src/main/scala/im/actor/server/office/PushTexts.scala deleted file mode 100644 index 95603ffe4e..0000000000 --- a/actor-server/actor-core/src/main/scala/im/actor/server/office/PushTexts.scala +++ /dev/null @@ -1,11 +0,0 @@ -package im.actor.server.office - -object PushTexts { - val Added = "User added" - val Invited = "You are invited to a group" - val Kicked = "User kicked" - val Left = "User left" - val TitleChanged = "Group title changed" - val TopicChanged = "Group topic changed" - val AboutChanged = "Group about changed" -} \ No newline at end of file diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/presences/GroupPresenceManager.scala b/actor-server/actor-core/src/main/scala/im/actor/server/presences/GroupPresenceManager.scala index 2d2142d4bc..c4041aed4d 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/presences/GroupPresenceManager.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/presences/GroupPresenceManager.scala @@ -146,4 +146,4 @@ class GroupPresenceManager extends Actor with ActorLogging with Stash { private def deliverState(groupId: Int, consumer: ActorRef): Unit = consumer ! GroupPresenceState(groupId, onlineUserIds.size) -} \ No newline at end of file +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/push/PushProvider.scala b/actor-server/actor-core/src/main/scala/im/actor/server/push/PushProvider.scala new file mode 100644 index 0000000000..126ebfc967 --- /dev/null +++ b/actor-server/actor-core/src/main/scala/im/actor/server/push/PushProvider.scala @@ -0,0 +1,3 @@ +package im.actor.server.push + +private[push] trait PushProvider diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/push/actor/ActorPush.scala b/actor-server/actor-core/src/main/scala/im/actor/server/push/actor/ActorPush.scala index b2608b994d..feb1f590ca 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/push/actor/ActorPush.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/push/actor/ActorPush.scala @@ -23,7 +23,7 @@ final case class ActorPushMessage(data: JsonObject) object ActorPushMessage { def apply(fields: Map[String, String]): ActorPushMessage = - ActorPushMessage(JsonObject.fromMap(fields mapValues Json.string)) + ActorPushMessage(JsonObject.fromMap(fields mapValues Json.fromString)) def apply(fields: (String, String)*): ActorPushMessage = ActorPushMessage(Map(fields: _*)) @@ -70,7 +70,7 @@ final class ActorPush(system: ActorSystem) extends Extension { } def deliver(seq: Int, creds: ActorPushCredentials): Unit = - deliver(ActorPushMessage(JsonObject.singleton("seq", Json.int(seq))), creds) + deliver(ActorPushMessage(JsonObject.singleton("seq", Json.fromInt(seq))), creds) def deliver(message: ActorPushMessage, creds: ActorPushCredentials): Unit = { val uri = Uri.parseAbsolute(ParserInput(creds.endpoint)) @@ -92,4 +92,4 @@ object ActorPush extends ExtensionId[ActorPush] with ExtensionIdProvider { override def createExtension(system: ExtendedActorSystem): ActorPush = new ActorPush(system) override def lookup(): ExtensionId[_ <: Extension] = ActorPush -} \ No newline at end of file +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/APNSSend.scala b/actor-server/actor-core/src/main/scala/im/actor/server/push/apple/APNSSend.scala similarity index 98% rename from actor-server/actor-core/src/main/scala/im/actor/server/sequence/APNSSend.scala rename to actor-server/actor-core/src/main/scala/im/actor/server/push/apple/APNSSend.scala index dac84bad73..825b83fbe3 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/APNSSend.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/push/apple/APNSSend.scala @@ -1,4 +1,4 @@ -package im.actor.server.sequence +package im.actor.server.push.apple import akka.actor.ActorSystem import com.google.protobuf.wrappers.{ Int32Value, StringValue } diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/ApplePushConfig.scala b/actor-server/actor-core/src/main/scala/im/actor/server/push/apple/ApplePushConfig.scala similarity index 96% rename from actor-server/actor-core/src/main/scala/im/actor/server/sequence/ApplePushConfig.scala rename to actor-server/actor-core/src/main/scala/im/actor/server/push/apple/ApplePushConfig.scala index 57bc5e3f5c..dabea62cea 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/ApplePushConfig.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/push/apple/ApplePushConfig.scala @@ -1,7 +1,7 @@ -package im.actor.server.sequence +package im.actor.server.push.apple -import com.typesafe.config.Config import com.github.kxbmap.configs.syntax._ +import com.typesafe.config.Config import scala.collection.JavaConversions._ @@ -34,4 +34,4 @@ object ApnsCert { isVoip = config.getOrElse[Boolean]("voip", false) ) } -} \ No newline at end of file +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/ApplePushExtension.scala b/actor-server/actor-core/src/main/scala/im/actor/server/push/apple/ApplePushExtension.scala similarity index 97% rename from actor-server/actor-core/src/main/scala/im/actor/server/sequence/ApplePushExtension.scala rename to actor-server/actor-core/src/main/scala/im/actor/server/push/apple/ApplePushExtension.scala index ff0cfb4dd0..04f12cab82 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/ApplePushExtension.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/push/apple/ApplePushExtension.scala @@ -1,4 +1,4 @@ -package im.actor.server.sequence +package im.actor.server.push.apple import java.io.File import java.util.concurrent.{ ExecutionException, TimeUnit, TimeoutException } @@ -13,9 +13,8 @@ import im.actor.server.persist.push.ApplePushCredentialsRepo import im.actor.util.log.AnyRefLogSource import scala.collection.concurrent.TrieMap -import scala.concurrent.Future -import scala.concurrent.blocking import scala.concurrent.duration._ +import scala.concurrent.{ Future, blocking } import scala.util.Try object ApplePushExtension extends ExtensionId[ApplePushExtension] with ExtensionIdProvider { diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/ApplePushProvider.scala b/actor-server/actor-core/src/main/scala/im/actor/server/push/apple/ApplePushProvider.scala similarity index 73% rename from actor-server/actor-core/src/main/scala/im/actor/server/sequence/ApplePushProvider.scala rename to actor-server/actor-core/src/main/scala/im/actor/server/push/apple/ApplePushProvider.scala index a3f261ea35..135eebb2f2 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/ApplePushProvider.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/push/apple/ApplePushProvider.scala @@ -1,14 +1,15 @@ -package im.actor.server.sequence +package im.actor.server.push.apple import akka.actor.ActorSystem import akka.event.Logging -import com.google.protobuf.wrappers.{ Int32Value, StringValue } import com.relayrides.pushy.apns.ApnsClient import com.relayrides.pushy.apns.util.{ ApnsPayloadBuilder, SimpleApnsPushNotification } import im.actor.server.dialog.DialogExtension import im.actor.server.model.push.ApplePushCredentials +import im.actor.server.push.PushProvider +import im.actor.server.sequence.PushData -private[sequence] final class ApplePushProvider(userId: Int)(implicit system: ActorSystem) extends PushProvider with APNSSend { +final class ApplePushProvider(userId: Int)(implicit system: ActorSystem) extends PushProvider with APNSSend { import system.dispatcher private val log = Logging(system, getClass) @@ -44,21 +45,24 @@ private[sequence] final class ApplePushProvider(userId: Int)(implicit system: Ac ): Unit = { withClient(creds) { implicit client ⇒ if (isLegacyCreds(creds)) { - val builder = - new ApnsPayloadBuilder() - .addCustomProperty("seq", seq) - .setContentAvailable(true) + dialogExt.getUnreadTotal(userId) foreach { total ⇒ + val builder = + new ApnsPayloadBuilder() + .addCustomProperty("seq", seq) + .setContentAvailable(true) + .setBadgeNumber(total) - if (data.text.nonEmpty && isTextEnabled) - builder.setAlertBody(data.text) - else if (data.censoredText.nonEmpty) - builder.setAlertBody(data.censoredText) + if (data.text.nonEmpty && isTextEnabled) + builder.setAlertBody(data.text) + else if (data.censoredText.nonEmpty) + builder.setAlertBody(data.censoredText) - if (isSoundEnabled) - builder.setSoundFileName(customSound getOrElse "iapetus.caf") + if (isSoundEnabled) + builder.setSoundFileName(customSound getOrElse "iapetus.caf") - val payload = builder.buildWithDefaultMaximumLength() - sendNotification(payload, creds, userId) + val payload = builder.buildWithDefaultMaximumLength() + sendNotification(payload, creds, userId) + } } else { sendNotification(payload = seqOnly(seq), creds, userId) } @@ -81,4 +85,4 @@ private[sequence] final class ApplePushProvider(userId: Int)(implicit system: Ac } } -} \ No newline at end of file +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/PushFutureListener.scala b/actor-server/actor-core/src/main/scala/im/actor/server/push/apple/PushFutureListener.scala similarity index 95% rename from actor-server/actor-core/src/main/scala/im/actor/server/sequence/PushFutureListener.scala rename to actor-server/actor-core/src/main/scala/im/actor/server/push/apple/PushFutureListener.scala index 5ad356be26..58e89e02cc 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/PushFutureListener.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/push/apple/PushFutureListener.scala @@ -1,10 +1,11 @@ -package im.actor.server.sequence +package im.actor.server.push.apple import akka.actor.ActorSystem import akka.event.Logging import com.relayrides.pushy.apns.PushNotificationResponse import com.relayrides.pushy.apns.util.SimpleApnsPushNotification import im.actor.server.model.push.ApplePushCredentials +import im.actor.server.sequence.SeqUpdatesExtension import im.actor.util.log.AnyRefLogSource import io.netty.util.concurrent.{ Future, GenericFutureListener } import scodec.bits.BitVector diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/push/google/DeliveryStream.scala b/actor-server/actor-core/src/main/scala/im/actor/server/push/google/DeliveryStream.scala new file mode 100644 index 0000000000..09450343c8 --- /dev/null +++ b/actor-server/actor-core/src/main/scala/im/actor/server/push/google/DeliveryStream.scala @@ -0,0 +1,77 @@ +package im.actor.server.push.google + +import akka.actor.{ ActorRef, ActorSystem } +import akka.event.Logging +import akka.stream.actor.ActorPublisher +import akka.stream.scaladsl.{ Flow, Source } +import akka.{ Done, NotUsed } +import cats.data.Xor +import im.actor.server.push.google.GooglePushDelivery.Delivery +import io.circe.parser +import spray.client.pipelining._ +import spray.http.{ HttpCharsets, StatusCodes } + +import scala.concurrent.Future +import scala.util.{ Failure, Success } + +private[google] final class DeliveryStream(publisher: ActorRef, serviceName: String, remove: String ⇒ Future[_])(implicit system: ActorSystem) { + import system.dispatcher + + private val log = Logging(system, getClass) + + private implicit val mat = tolerantMaterializer + + log.debug("Starting {} stream", serviceName) + + val stream: Future[Done] = Source + .fromPublisher(ActorPublisher[NotificationDelivery](publisher)) + .via(flow) + .runForeach { + // TODO: flatten + case Xor.Right((body, delivery)) ⇒ + parser.parse(body) match { + case Xor.Right(json) ⇒ + json.asObject match { + case Some(obj) ⇒ + obj("error") flatMap (_.asString) match { + case Some("InvalidRegistration") ⇒ + log.warning("{}: Invalid registration, deleting", serviceName) + remove(delivery.m.to) + case Some("NotRegistered") ⇒ + log.warning("{}: Token is not registered, deleting", serviceName) + remove(delivery.m.to) + case Some(other) ⇒ + log.warning("{}: Error in response: {}", serviceName, other) + case None ⇒ + log.debug("{}: Successfully delivered: {}", serviceName, delivery) + } + case None ⇒ + log.error("{}: Expected JSON Object but got: {}", serviceName, json) + } + case Xor.Left(failure) ⇒ log.error(failure.underlying, "{}: Failed to parse response", serviceName) + } + case Xor.Left(e) ⇒ + log.error(e, "{}: Failed to make request", serviceName) + } + + stream onComplete { + case Failure(e) ⇒ + log.error(e, "{}: Failure in stream", serviceName) + case Success(_) ⇒ + log.debug("{}: Stream completed", serviceName) + } + + private def flow(implicit system: ActorSystem): Flow[NotificationDelivery, Xor[RuntimeException, (String, Delivery)], NotUsed] = { + import system.dispatcher + val pipeline = sendReceive + Flow[NotificationDelivery].mapAsync(2) { + case (req, del) ⇒ + pipeline(req) map { resp ⇒ + if (resp.status == StatusCodes.OK) + Xor.Right(resp.entity.data.asString(HttpCharsets.`UTF-8`) → del) + else + Xor.Left(new RuntimeException(s"Failed to deliver message, StatusCode was not OK: ${resp.status}")) + } + } + } +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/push/google/FirebasePushExtension.scala b/actor-server/actor-core/src/main/scala/im/actor/server/push/google/FirebasePushExtension.scala new file mode 100644 index 0000000000..14b3cc12d7 --- /dev/null +++ b/actor-server/actor-core/src/main/scala/im/actor/server/push/google/FirebasePushExtension.scala @@ -0,0 +1,42 @@ +package im.actor.server.push.google + +import akka.actor._ +import akka.event.Logging +import im.actor.server.model.push.FirebasePushCredentials +import im.actor.server.persist.push.FirebasePushCredentialsKV + +import scala.concurrent.Future + +object FirebasePushExtension extends ExtensionId[FirebasePushExtension] with ExtensionIdProvider { + override def createExtension(system: ExtendedActorSystem): FirebasePushExtension = new FirebasePushExtension(system) + + override def lookup(): ExtensionId[_ <: Extension] = FirebasePushExtension +} + +final class FirebasePushExtension(system: ActorSystem) extends Extension { + + private val log = Logging(system, getClass) + + private val firebaseKV = new FirebasePushCredentialsKV()(system) + + private val config = GooglePushManagerConfig.loadFirebase.get + + private val firebasePublisher = system.actorOf(GooglePushDelivery.props("https://fcm.googleapis.com/fcm/send"), "fcm-delivery") + + private val firebaseStream = new DeliveryStream(firebasePublisher, "Firebase", remove)(system).stream + + def send(projectId: Long, message: GooglePushMessage): Unit = + config.keyMap get projectId match { + case Some(key) ⇒ + firebasePublisher ! GooglePushDelivery.Delivery(message, key) + case None ⇒ + log.warning("Key not found for projectId: {}", projectId) + } + + private def remove(regId: String): Future[Unit] = + firebaseKV.deleteByToken(regId) + + def fetchCreds(authIds: Set[Long]): Future[Seq[FirebasePushCredentials]] = + firebaseKV.find(authIds) + +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/push/google/GCMPushExtension.scala b/actor-server/actor-core/src/main/scala/im/actor/server/push/google/GCMPushExtension.scala new file mode 100644 index 0000000000..9386497dad --- /dev/null +++ b/actor-server/actor-core/src/main/scala/im/actor/server/push/google/GCMPushExtension.scala @@ -0,0 +1,41 @@ +package im.actor.server.push.google + +import akka.actor._ +import akka.event.Logging +import im.actor.server.db.DbExtension +import im.actor.server.model.push.GCMPushCredentials +import im.actor.server.persist.push.GooglePushCredentialsRepo + +import scala.concurrent.Future + +object GCMPushExtension extends ExtensionId[GCMPushExtension] with ExtensionIdProvider { + override def createExtension(system: ExtendedActorSystem): GCMPushExtension = new GCMPushExtension(system) + + override def lookup(): ExtensionId[_ <: Extension] = GCMPushExtension +} + +final class GCMPushExtension(system: ActorSystem) extends Extension { + + private val log = Logging(system, getClass) + private val db = DbExtension(system).db + + private val config = GooglePushManagerConfig.loadGCM.get + + private val gcmPublisher = system.actorOf(GooglePushDelivery.props("https://gcm-http.googleapis.com/gcm/send"), "gcm-delivery") + + private val gcmStream = new DeliveryStream(gcmPublisher, "GCM", remove)(system).stream + + def send(projectId: Long, message: GooglePushMessage): Unit = + config.keyMap get projectId match { + case Some(key) ⇒ + gcmPublisher ! GooglePushDelivery.Delivery(message, key) + case None ⇒ + log.warning("Key not found for projectId: {}", projectId) + } + + private def remove(regId: String): Future[Int] = + db.run(GooglePushCredentialsRepo.deleteByToken(regId)) + + def fetchCreds(authIds: Set[Long]): Future[Seq[GCMPushCredentials]] = + db.run(GooglePushCredentialsRepo.find(authIds)) +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/push/google/GooglePushDelivery.scala b/actor-server/actor-core/src/main/scala/im/actor/server/push/google/GooglePushDelivery.scala new file mode 100644 index 0000000000..278b63ba9a --- /dev/null +++ b/actor-server/actor-core/src/main/scala/im/actor/server/push/google/GooglePushDelivery.scala @@ -0,0 +1,66 @@ +package im.actor.server.push.google + +import akka.actor.{ ActorLogging, Props } +import akka.stream.actor.{ ActorPublisher, ActorPublisherMessage } +import io.circe.generic.auto._ +import io.circe.syntax._ +import spray.http.HttpHeaders.Authorization +import spray.http.HttpMethods.POST +import spray.http._ + +import scala.annotation.tailrec + +private[google] object GooglePushDelivery { + final case class Delivery(m: GooglePushMessage, key: String) + + private val MaxQueue = 100000 + + def props(apiUri: String) = Props(classOf[GooglePushDelivery], apiUri) +} + +private final class GooglePushDelivery(apiUri: String) extends ActorPublisher[NotificationDelivery] with ActorLogging { + import ActorPublisherMessage._ + import GooglePushDelivery._ + + private[this] var buf = Vector.empty[NotificationDelivery] + private[this] val uri = Uri(apiUri) + + def receive = { + case d: Delivery if buf.size == MaxQueue ⇒ + log.error("Current queue is already at size MaxQueue: {}, totalDemand: {}, ignoring delivery", MaxQueue, totalDemand) + deliverBuf() + case d: Delivery ⇒ + log.debug("Trying to deliver google push. Queue size: {}, totalDemand: {}", buf.size, totalDemand) + if (buf.isEmpty && totalDemand > 0) { + onNext(mkJob(d)) + } else { + this.buf :+= mkJob(d) + deliverBuf() + } + case Request(n) ⇒ + log.debug("Trying to deliver google push. Queue size: {}, totalDemand: {}, subscriber requests {} elements", buf.size, totalDemand, n) + deliverBuf() + } + + @tailrec private def deliverBuf(): Unit = + if (totalDemand > 0) { + if (totalDemand <= Int.MaxValue) { + val (use, keep) = buf.splitAt(totalDemand.toInt) + buf = keep + use foreach onNext + } else { + val (use, keep) = buf.splitAt(Int.MaxValue) + buf = keep + use foreach onNext + deliverBuf() + } + } + + private def mkJob(d: Delivery): NotificationDelivery = + HttpRequest( + method = POST, + uri = uri, + headers = List(Authorization(GenericHttpCredentials(s"key=${d.key}", Map.empty[String, String]))), + entity = HttpEntity(ContentTypes.`application/json`, d.m.asJson.noSpaces) + ) → d +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/push/google/GooglePushMessage.scala b/actor-server/actor-core/src/main/scala/im/actor/server/push/google/GooglePushMessage.scala new file mode 100644 index 0000000000..a95d276fe4 --- /dev/null +++ b/actor-server/actor-core/src/main/scala/im/actor/server/push/google/GooglePushMessage.scala @@ -0,0 +1,8 @@ +package im.actor.server.push.google + +final case class GooglePushMessage( + to: String, + collapse_key: Option[String], + data: Option[Map[String, String]], + time_to_live: Option[Int] +) diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/push/google/GooglePushProvider.scala b/actor-server/actor-core/src/main/scala/im/actor/server/push/google/GooglePushProvider.scala new file mode 100644 index 0000000000..7d71d9eac3 --- /dev/null +++ b/actor-server/actor-core/src/main/scala/im/actor/server/push/google/GooglePushProvider.scala @@ -0,0 +1,62 @@ +package im.actor.server.push.google + +import akka.actor.ActorSystem +import im.actor.server.model.push.{ FirebasePushCredentials, GCMPushCredentials, GooglePushCredentials } +import im.actor.server.push.PushProvider +import im.actor.server.sequence.PushData + +final class GooglePushProvider(userId: Int, system: ActorSystem) extends PushProvider { + private val gcmPushExt = GCMPushExtension(system) + private val firebasePushExt = FirebasePushExtension(system) + + def deliverInvisible(seq: Int, creds: GooglePushCredentials): Unit = { + val message = GooglePushMessage( + to = creds.regId, + collapse_key = Some(s"seq-invisible-${userId.toString}"), + data = Some( + Map( + "seq" → seq.toString, + "_authId" → creds.authId.toString + ) + ), + time_to_live = None + ) + creds match { + case _: GCMPushCredentials ⇒ + gcmPushExt.send(creds.projectId, message) + case _: FirebasePushCredentials ⇒ + firebasePushExt.send(creds.projectId, message) + } + } + + def deliverVisible( + seq: Int, + creds: GooglePushCredentials, + data: PushData, + isTextEnabled: Boolean, + isSoundEnabled: Boolean, + isVibrationEnabled: Boolean + ): Unit = { + val message = GooglePushMessage( + to = creds.regId, + collapse_key = Some(s"seq-visible-${userId.toString}"), + data = Some( + Map( + "seq" → seq.toString, + "_authId" → creds.authId.toString + ) ++ (data.text match { + case text if text.nonEmpty && isTextEnabled ⇒ + Map("message" → text) + case _ ⇒ Map.empty + }) + ), + time_to_live = None + ) + creds match { + case _: GCMPushCredentials ⇒ + gcmPushExt.send(creds.projectId, message) + case _: FirebasePushCredentials ⇒ + firebasePushExt.send(creds.projectId, message) + } + } +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/push/google/configs.scala b/actor-server/actor-core/src/main/scala/im/actor/server/push/google/configs.scala new file mode 100644 index 0000000000..a509251158 --- /dev/null +++ b/actor-server/actor-core/src/main/scala/im/actor/server/push/google/configs.scala @@ -0,0 +1,31 @@ +package im.actor.server.push.google + +import com.typesafe.config.Config +import com.github.kxbmap.configs.syntax._ +import im.actor.config.ActorConfig + +import scala.util.Try + +private final case class GooglePushKey(projectId: Long, key: String) + +private[google] final case class GooglePushManagerConfig(keys: List[GooglePushKey]) { + val keyMap: Map[Long, String] = + (keys map { + case GooglePushKey(projectId, key) ⇒ projectId → key + }).toMap +} + +private[google] object GooglePushManagerConfig { + + def loadGCM: Try[GooglePushManagerConfig] = { + val config = ActorConfig.load() + val legacy = load(config.getConfig("services.google.push")) + val gcm = load(config.getConfig("services.google.gcm")) + legacy.orElse(gcm) + } + + def loadFirebase: Try[GooglePushManagerConfig] = + load(ActorConfig.load().getConfig("services.google.firebase")) + + private def load(config: ⇒ Config) = Try(config.extract[GooglePushManagerConfig]) +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/push/google/package.scala b/actor-server/actor-core/src/main/scala/im/actor/server/push/google/package.scala new file mode 100644 index 0000000000..2526361b31 --- /dev/null +++ b/actor-server/actor-core/src/main/scala/im/actor/server/push/google/package.scala @@ -0,0 +1,29 @@ +package im.actor.server.push + +import akka.actor.ActorSystem +import akka.stream.{ ActorMaterializer, ActorMaterializerSettings, Supervision } +import spray.http.HttpRequest + +import scala.concurrent._ + +package object google { + + type NotificationDelivery = (HttpRequest, GooglePushDelivery.Delivery) + + def tolerantMaterializer(implicit system: ActorSystem): ActorMaterializer = { + val streamDecider: Supervision.Decider = { + case e: TimeoutException ⇒ + system.log.warning("Timeout in stream, RESUME {}", e) + Supervision.Resume + case e: RuntimeException ⇒ + system.log.warning("Got runtime exception in stream, RESUME {}", e) + Supervision.Resume + case e ⇒ + system.log.error(e, "Got exception in stream, STOP") + Supervision.Stop + } + ActorMaterializer(ActorMaterializerSettings(system) + .withSupervisionStrategy(streamDecider)) + } + +} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/GooglePushExtension.scala b/actor-server/actor-core/src/main/scala/im/actor/server/sequence/GooglePushExtension.scala deleted file mode 100644 index 690a816494..0000000000 --- a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/GooglePushExtension.scala +++ /dev/null @@ -1,209 +0,0 @@ -package im.actor.server.sequence - -import akka.NotUsed -import akka.actor._ -import akka.event.Logging -import akka.stream.{ ActorMaterializer, ActorMaterializerSettings, Supervision } -import akka.stream.actor.{ ActorPublisher, ActorPublisherMessage } -import akka.stream.scaladsl.{ Flow, Source } -import cats.data.Xor -import com.github.kxbmap.configs.syntax._ -import com.typesafe.config.Config -import im.actor.server.db.DbExtension -import im.actor.server.model.push.GooglePushCredentials -import im.actor.server.persist.push.GooglePushCredentialsRepo -import io.circe.generic.auto._ -import io.circe.jawn._ -import io.circe.syntax._ -import spray.client.pipelining._ -import spray.http.HttpHeaders.Authorization -import spray.http._ - -import scala.annotation.tailrec -import scala.concurrent.{ Future, TimeoutException } -import scala.util.{ Failure, Success, Try } - -private final case class GooglePushKey(projectId: Long, key: String) - -private object GooglePushKey { - def load(config: Config): Try[GooglePushKey] = { - for { - projectId ← config.get[Try[Long]]("project-id") - key ← config.get[Try[String]]("key") - } yield GooglePushKey(projectId, key) - } -} - -private final case class GooglePushManagerConfig(keys: List[GooglePushKey]) - -private object GooglePushManagerConfig { - def load(googlePushConfig: Config): Try[GooglePushManagerConfig] = - for { - keyConfigs ← googlePushConfig.get[Try[List[Config]]]("keys") - keys ← Try(keyConfigs map (GooglePushKey.load(_).get)) - } yield GooglePushManagerConfig(keys) -} - -final case class GooglePushMessage( - to: String, - collapse_key: Option[String], - data: Option[Map[String, String]], - time_to_live: Option[Int] -) - -object GooglePushExtension extends ExtensionId[GooglePushExtension] with ExtensionIdProvider { - override def createExtension(system: ExtendedActorSystem): GooglePushExtension = new GooglePushExtension(system) - - override def lookup(): ExtensionId[_ <: Extension] = GooglePushExtension -} - -final class GooglePushExtension(system: ActorSystem) extends Extension { - - import system.dispatcher - - private implicit val _system = system - private val log = Logging(system, getClass) - private val db = DbExtension(system).db - - private val streamDecider: Supervision.Decider = { - case e: TimeoutException ⇒ - log.warning("Got timeout exception in stream, RESUME {}", e) - Supervision.Resume - case e: RuntimeException ⇒ - log.warning("Got runtime exception in stream, RESUME {}", e) - Supervision.Resume - case e ⇒ - log.error(e, "Got exception in stream, STOP") - Supervision.Stop - } - private implicit val mat = ActorMaterializer(ActorMaterializerSettings(system).withSupervisionStrategy(streamDecider)) - - private val config = GooglePushManagerConfig.load(system.settings.config.getConfig("services.google.push")).get - private val deliveryPublisher = system.actorOf(GooglePushDelivery.props, "google-push-delivery") - - Source.fromPublisher(ActorPublisher[(HttpRequest, GooglePushDelivery.Delivery)](deliveryPublisher)) - .via(GooglePushDelivery.flow) - .runForeach { - // TODO: flatten - case Xor.Right((body, delivery)) ⇒ - parse(body) match { - case Xor.Right(json) ⇒ - json.asObject match { - case Some(obj) ⇒ - obj("error") flatMap (_.asString) match { - case Some("InvalidRegistration") ⇒ - log.warning("Invalid registration, deleting") - remove(delivery.m.to) - case Some("NotRegistered") ⇒ - log.warning("Token is not registered, deleting") - remove(delivery.m.to) - case Some(other) ⇒ - log.warning("Error in GCM response: {}", other) - case None ⇒ - log.debug("Successfully delivered: {}", delivery) - } - case None ⇒ - log.error("Expected JSON Object but got: {}", json) - } - case Xor.Left(failure) ⇒ log.error(failure.underlying, "Failed to parse response") - } - case Xor.Left(e) ⇒ - log.error(e, "Failed to make request") - } onComplete { - case Failure(e) ⇒ - log.error(e, "Failure in stream, continue with next element") - case Success(_) ⇒ log.debug("Stream completed") - } - - private def remove(regId: String): Future[Int] = db.run(GooglePushCredentialsRepo.deleteByToken(regId)) - - private val keys: Map[Long, String] = - (config.keys map { - case GooglePushKey(projectId, key) ⇒ projectId → key - }).toMap - - def send(projectId: Long, message: GooglePushMessage): Unit = - keys get projectId match { - case Some(key) ⇒ - deliveryPublisher ! GooglePushDelivery.Delivery(message, key) - case None ⇒ - log.warning("Key not found for projectId: {}", projectId) - } - - def fetchCreds(authIds: Set[Long]): Future[Seq[GooglePushCredentials]] = - db.run(GooglePushCredentialsRepo.find(authIds)) -} - -private object GooglePushDelivery { - - object Tick - - final case class Delivery(m: GooglePushMessage, key: String) - - private val MaxQueue = 100000 - - def props = Props(classOf[GooglePushDelivery]) - - def flow(implicit system: ActorSystem): Flow[(HttpRequest, Delivery), Xor[RuntimeException, (String, Delivery)], NotUsed] = { - import system.dispatcher - val pipeline = sendReceive - Flow[(HttpRequest, GooglePushDelivery.Delivery)].mapAsync(2) { - case (req, del) ⇒ - pipeline(req) map { resp ⇒ - if (resp.status == StatusCodes.OK) - Xor.Right(resp.entity.data.asString(HttpCharsets.`UTF-8`) → del) - else - Xor.Left(new RuntimeException(s"Failed to deliver message, StatusCode was not OK: ${resp.status}")) - } - } - } -} - -private final class GooglePushDelivery extends ActorPublisher[(HttpRequest, GooglePushDelivery.Delivery)] with ActorLogging { - - import GooglePushDelivery._ - import ActorPublisherMessage._ - - private[this] var buf = Vector.empty[(HttpRequest, Delivery)] - private val uri = Uri("https://gcm-http.googleapis.com/gcm/send") - - def receive = { - case d: Delivery if buf.size == MaxQueue ⇒ - log.error("Current queue is already at size MaxQueue: {}, totalDemand: {}, ignoring delivery", MaxQueue, totalDemand) - deliverBuf() - case d: Delivery ⇒ - log.debug("Trying to deliver google push. Queue size: {}, totalDemand: {}", buf.size, totalDemand) - if (buf.isEmpty && totalDemand > 0) { - onNext(mkJob(d)) - } else { - this.buf :+= mkJob(d) - deliverBuf() - } - case Request(n) ⇒ - log.debug("Trying to deliver google push. Queue size: {}, totalDemand: {}, subscriber requests {} elements", buf.size, totalDemand, n) - deliverBuf() - } - - @tailrec def deliverBuf(): Unit = - if (totalDemand > 0) { - if (totalDemand <= Int.MaxValue) { - val (use, keep) = buf.splitAt(totalDemand.toInt) - buf = keep - use foreach onNext - } else { - val (use, keep) = buf.splitAt(Int.MaxValue) - buf = keep - use foreach onNext - deliverBuf() - } - } - - private def mkJob(d: Delivery): (HttpRequest, Delivery) = { - HttpRequest( - method = HttpMethods.POST, - uri = uri, - headers = List(Authorization(GenericHttpCredentials(s"key=${d.key}", Map.empty[String, String]))), - entity = HttpEntity(ContentTypes.`application/json`, d.m.asJson.noSpaces) - ) → d - } -} diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/GooglePushProvider.scala b/actor-server/actor-core/src/main/scala/im/actor/server/sequence/GooglePushProvider.scala deleted file mode 100644 index 8e4d6a437a..0000000000 --- a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/GooglePushProvider.scala +++ /dev/null @@ -1,43 +0,0 @@ -package im.actor.server.sequence - -import akka.actor.ActorSystem -import im.actor.server.model.push.GooglePushCredentials - -private[sequence] final class GooglePushProvider(userId: Int, system: ActorSystem) extends PushProvider { - private val googlePushExt = GooglePushExtension(system) - - def deliverInvisible(seq: Int, creds: GooglePushCredentials): Unit = { - val message = GooglePushMessage( - to = creds.regId, - collapse_key = Some(s"seq-invisible-${userId.toString}"), - data = Some(Map("seq" → seq.toString)), - time_to_live = None - ) - - googlePushExt.send(creds.projectId, message) - } - - def deliverVisible( - seq: Int, - creds: GooglePushCredentials, - data: PushData, - isTextEnabled: Boolean, - isSoundEnabled: Boolean, - isVibrationEnabled: Boolean - ): Unit = { - val message = GooglePushMessage( - to = creds.regId, - collapse_key = Some(s"seq-visible-${userId.toString}"), - data = Some(Map("seq" → seq.toString) ++ ( - data.text match { - case text if text.nonEmpty && isTextEnabled ⇒ - Map("message" → text) - case _ ⇒ Map.empty - } - )), - time_to_live = None - ) - - googlePushExt.send(creds.projectId, message) - } -} \ No newline at end of file diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/Optimization.scala b/actor-server/actor-core/src/main/scala/im/actor/server/sequence/Optimization.scala index 6ae868af4f..01f6e81092 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/Optimization.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/sequence/Optimization.scala @@ -3,6 +3,7 @@ package im.actor.server.sequence import com.google.protobuf.ByteString import im.actor.api.rpc.counters.UpdateCountersChanged import im.actor.api.rpc.groups._ +import im.actor.api.rpc.messaging.UpdateChatDropCache import im.actor.api.rpc.sequence.{ ApiUpdateOptimization, UpdateEmptyUpdate } import im.actor.server.messaging.MessageParsing import im.actor.server.model.SerializedUpdate @@ -36,15 +37,16 @@ object Optimization extends MessageParsing { UpdateGroupOwnerChanged.header, UpdateGroupHistoryShared.header, - UpdateGroupCanSendMessagesChanged.header, - UpdateGroupCanViewMembersChanged.header, - UpdateGroupCanInviteMembersChanged.header, UpdateGroupMemberChanged.header, UpdateGroupMembersBecameAsync.header, UpdateGroupMembersUpdated.header, UpdateGroupMemberDiff.header, UpdateGroupMembersCountChanged.header, - UpdateGroupMemberAdminChanged.header + UpdateGroupMemberAdminChanged.header, + UpdateGroupShortNameChanged.header, + UpdateGroupFullPermissionsChanged.header, + UpdateGroupPermissionsChanged.header, + UpdateChatDropCache.header ) if (deliveryTag == GroupV2) emptyUpdate diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/PushProvider.scala b/actor-server/actor-core/src/main/scala/im/actor/server/sequence/PushProvider.scala deleted file mode 100644 index 185948907c..0000000000 --- a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/PushProvider.scala +++ /dev/null @@ -1,3 +0,0 @@ -package im.actor.server.sequence - -private[sequence] trait PushProvider diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/SeqUpdatesExtension.scala b/actor-server/actor-core/src/main/scala/im/actor/server/sequence/SeqUpdatesExtension.scala index 9f0b5ede23..ee6de9f22c 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/SeqUpdatesExtension.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/sequence/SeqUpdatesExtension.scala @@ -10,6 +10,7 @@ import com.google.protobuf.wrappers.Int32Value import im.actor.server.db.DbExtension import im.actor.server.migrations.v2.{ MigrationNameList, MigrationTsActions } import im.actor.server.model._ +import im.actor.server.persist.push.FirebasePushCredentialsKV import im.actor.server.sequence.operations.{ DeliveryOperations, DifferenceOperations, PushOperations } import im.actor.storage.SimpleStorage import im.actor.storage.api.{ GetAction, UpsertAction } @@ -57,12 +58,16 @@ final class SeqUpdatesExtension(_system: ActorSystem) lazy val region: SeqUpdatesManagerRegion = SeqUpdatesManagerRegion.start()(system) private val writer = system.actorOf(BatchUpdatesWriter.props, "batch-updates-writer") private val mediator = DistributedPubSub(system).mediator + protected lazy val firebaseKv = new FirebasePushCredentialsKV val MultiSequenceMigrationTs: Long = { val optTs = MigrationTsActions.getTimestamp(MigrationNameList.MultiSequence)(conn) optTs.getOrElse(throw new RuntimeException(s"No Migration timestamp found for ${MigrationNameList.MultiSequence}")) } + def msgDeliveryId(peer: Peer, randomId: Long) = + s"msg_${peer.`type`.value}_${peer.id}_${randomId}" + def getSeqState(userId: Int, authId: Long): Future[SeqState] = (region.ref ? Envelope(userId).withGetSeqState(GetSeqState(authId))).mapTo[SeqState] diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/VendorPush.scala b/actor-server/actor-core/src/main/scala/im/actor/server/sequence/VendorPush.scala index b36cebd8ca..08cfeb72b2 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/VendorPush.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/sequence/VendorPush.scala @@ -2,19 +2,21 @@ package im.actor.server.sequence import akka.actor._ import akka.pattern.pipe +import im.actor.concurrent.FutureExt import im.actor.server.db.DbExtension -import im.actor.server.model.push.{ ActorPushCredentials, ApplePushCredentials, GooglePushCredentials, PushCredentials } +import im.actor.server.model.push._ import im.actor.server.model.{ DeviceType, Peer, PeerType } -import im.actor.server.persist.AuthSessionRepo +import im.actor.server.persist.{ AuthIdRepo, AuthSessionRepo } import im.actor.server.persist.configs.ParameterRepo -import im.actor.server.persist.push.{ ActorPushCredentialsRepo, ApplePushCredentialsRepo, GooglePushCredentialsRepo } +import im.actor.server.persist.push.{ ActorPushCredentialsRepo, ApplePushCredentialsRepo, FirebasePushCredentialsKV, GooglePushCredentialsRepo } import im.actor.server.push.actor.ActorPush +import im.actor.server.push.apple.ApplePushProvider +import im.actor.server.push.google.GooglePushProvider import im.actor.server.sequence.UserSequenceCommands.ReloadSettings import im.actor.server.userconfig.SettingsKeys import slick.dbio.DBIO import scala.concurrent.Future -import scala.util.control.NoStackTrace private[sequence] trait VendorPushCommand @@ -22,7 +24,13 @@ private final case class PushCredentialsInfo(appId: Int, authId: Long) private final case class AllNotificationSettings( generic: NotificationSettings = NotificationSettings(), - specific: Map[String, NotificationSettings] = Map.empty + specific: Map[String, NotificationSettings] = Map.empty, + groups: GroupNotificationSettings = GroupNotificationSettings() +) + +private final case class GroupNotificationSettings( + enabled: Boolean = true, + onlyMention: Boolean = false ) private final case class NotificationSettings( @@ -79,20 +87,29 @@ private final class SettingsControl(userId: Int) extends Actor with ActorLogging private def load(): Future[AllNotificationSettings] = db.run(for { - generic ← loadAction(DeviceType.Generic) - mobile ← loadAction(DeviceType.Mobile) - tablet ← loadAction(DeviceType.Tablet) - desktop ← loadAction(DeviceType.Desktop) + generic ← loadForDevice(DeviceType.Generic) + mobile ← loadForDevice(DeviceType.Mobile) + tablet ← loadForDevice(DeviceType.Tablet) + desktop ← loadForDevice(DeviceType.Desktop) + + groups ← loadForGroups() } yield AllNotificationSettings( generic = generic, specific = Map( DeviceType.Mobile → mobile, DeviceType.Tablet → tablet, DeviceType.Desktop → desktop - ) + ), + groups = groups )) - private def loadAction(deviceType: String): DBIO[NotificationSettings] = { + private def loadForGroups(): DBIO[GroupNotificationSettings] = + for { + enabled ← ParameterRepo.findBooleanValue(userId, SettingsKeys.accountGroupEnabled, true) + onlyMentions ← ParameterRepo.findBooleanValue(userId, SettingsKeys.accountGroupMentionEnabled, false) + } yield GroupNotificationSettings(enabled, onlyMentions) + + private def loadForDevice(deviceType: String): DBIO[NotificationSettings] = for { enabled ← ParameterRepo.findBooleanValue(userId, SettingsKeys.enabled(deviceType), true) sound ← ParameterRepo.findBooleanValue(userId, SettingsKeys.soundEnabled(deviceType), true) @@ -101,7 +118,7 @@ private final class SettingsControl(userId: Int) extends Actor with ActorLogging peers ← ParameterRepo.findPeerNotifications(userId, deviceType) customSounds ← ParameterRepo.findPeerRingtone(userId) } yield NotificationSettings(enabled, sound, vibration, text, customSounds.toMap, peers.toMap) - } + } private[sequence] final class VendorPush(userId: Int) extends Actor with ActorLogging with Stash { @@ -114,12 +131,16 @@ private[sequence] final class VendorPush(userId: Int) extends Actor with ActorLo private val settingsControl = context.actorOf(SettingsControl.props(userId), "settings") private val googlePushProvider = new GooglePushProvider(userId, context.system) + private val applePushProvider = new ApplePushProvider(userId)(context.system) private val actorPushProvider = ActorPush(context.system) + // TODO: why do we need `PushCredentialsInfo`, we have `authId` anyway! private var mapping: Map[PushCredentials, PushCredentialsInfo] = Map.empty private var notificationSettings = AllNotificationSettings() + private val firebaseKv = new FirebasePushCredentialsKV()(context.system) + init() def receive = initializing @@ -142,14 +163,18 @@ private[sequence] final class VendorPush(userId: Int) extends Actor with ActorLo register(r.getActor) case r: RegisterPushCredentials if r.creds.isApple ⇒ register(r.getApple) - case r: RegisterPushCredentials if r.creds.isGoogle ⇒ - register(r.getGoogle) + case r: RegisterPushCredentials if r.creds.isGcm ⇒ + register(r.getGcm) + case r: RegisterPushCredentials if r.creds.isFirebase ⇒ + register(r.getFirebase) case u: UnregisterPushCredentials if u.creds.isActor ⇒ unregister(u.getActor) case u: UnregisterPushCredentials if u.creds.isApple ⇒ unregister(u.getApple) - case u: UnregisterPushCredentials if u.creds.isGoogle ⇒ - unregister(u.getGoogle) + case u: UnregisterPushCredentials if u.creds.isGcm ⇒ + unregister(u.getGcm) + case u: UnregisterPushCredentials if u.creds.isFirebase ⇒ + unregister(u.getFirebase) case DeliverPush(authId, seq, rules) ⇒ deliver(authId, seq, rules.getOrElse(PushRules())) case r: ReloadSettings ⇒ @@ -165,18 +190,23 @@ private[sequence] final class VendorPush(userId: Int) extends Actor with ActorLo private def init(): Unit = { log.debug("Initializing") - db.run(for { - googleCreds ← GooglePushCredentialsRepo.findByUser(userId) - appleCreds ← ApplePushCredentialsRepo.findByUser(userId) - actorCreds ← ActorPushCredentialsRepo.findByUser(userId) - google ← DBIO.sequence(googleCreds map withInfo) map (_.flatten) - apple ← DBIO.sequence(appleCreds.filterNot(_.isVoip) map withInfo) map (_.flatten) - actor ← DBIO.sequence(actorCreds map withInfo) map (_.flatten) - } yield Initialized(apple ++ google ++ actor)) pipeTo self + (for { + modelAuthIds ← db.run(AuthIdRepo.findByUserId(userId)) + authIds = (modelAuthIds map (_.id)).toSet + gcmCreds ← db.run(GooglePushCredentialsRepo.find(authIds)) + firebaseCreds ← firebaseKv.find(authIds) + appleCreds ← db.run(ApplePushCredentialsRepo.find(authIds)) + actorCreds ← db.run(ActorPushCredentialsRepo.find(authIds)) + + gcm ← FutureExt.ftraverse(gcmCreds)(withInfo) map (_.flatten) + firebase ← FutureExt.ftraverse(firebaseCreds)(withInfo) map (_.flatten) + apple ← FutureExt.ftraverse(appleCreds.filterNot(_.isVoip))(withInfo) map (_.flatten) + actor ← FutureExt.ftraverse(actorCreds)(withInfo) map (_.flatten) + } yield Initialized(apple ++ gcm ++ actor)) pipeTo self } /** - * Delivers a push to credentials associated with given `authId` according to push `rules`` + * Delivers a push to credentials associated with given `authId` according to push `rules` * */ private def deliver(authId: Long, seq: Int, rules: PushRules): Unit = { @@ -200,33 +230,18 @@ private[sequence] final class VendorPush(userId: Int) extends Actor with ActorLo val deviceType = DeviceType(info.appId) if (rules.excludeAuthIds.contains(info.authId)) { - log.debug("AuthSid is excluded, not pushing") + log.debug("AuthId is excluded, not pushing") } else { rules.data match { case Some(data) ⇒ val settings = notificationSettings.specific.getOrElse(deviceType, notificationSettings.generic) - val isVisible = - (settings.enabled, data.peer) match { - case (true, Some(peer)) ⇒ - settings.peers.get(peer) match { - case Some(true) ⇒ - log.debug("Notifications for peer {} are enabled, push will be visible", peer) - true - case Some(false) ⇒ - log.debug("Notifications for peer {} are disabled, push will be invisible", peer) - false - case None ⇒ - log.debug("Notifications for peer {} are not set, push will be visible", peer) - true - } - case (true, None) ⇒ - log.debug("Notifications are enabled, delivering visible push") - true - case (false, _) ⇒ - log.debug("Notifications are disabled, delivering invisible push") - false - } + val isVisible = isNotificationVisible( + settings, + notificationSettings.groups, + data.peer, + data.isMentioned + ) if (isVisible) deliverVisible( @@ -248,6 +263,56 @@ private[sequence] final class VendorPush(userId: Int) extends Actor with ActorLo } } + private def isNotificationVisible( + settings: NotificationSettings, + groupSettings: GroupNotificationSettings, + optPeer: Option[Peer], + isMentioned: Boolean + ) = { + (settings.enabled, optPeer) match { + case (true, Some(peer)) ⇒ + peer.`type` match { + case PeerType.Group ⇒ + if (groupSettings.enabled) { + if (groupSettings.onlyMention) { + if (isMentioned) { + log.debug("User is mentioned, notification for group {} will be visible", peer) + true + } else { + log.debug("Message without mention, notification for group {} will be visible", peer) + false + } + } else { + log.debug("Group notifications are enabled, notification for group {} will be visible", peer) + true + } + } else { + log.debug("Group notifications are disabled, notification for group {} will be invisible", peer) + false + } + case _ ⇒ + settings.peers.get(peer) match { + case Some(true) ⇒ + log.debug("Notifications for peer {} are enabled, notification will be visible", peer) + true + case Some(false) ⇒ + log.debug("Notifications for peer {} are disabled, notification will be invisible", peer) + false + case None ⇒ + log.debug("Notifications for peer {} are not set, notification will be visible", peer) + true + } + + } + case (true, None) ⇒ + log.debug("Notifications are enabled, delivering visible push") + true + case (false, _) ⇒ + log.debug("Notifications are disabled, delivering invisible push") + false + } + } + /** * Delivers an invisible push with seq and contentAvailable * @@ -311,14 +376,12 @@ private[sequence] final class VendorPush(userId: Int) extends Actor with ActorLo } private def register(creds: PushCredentials): Unit = - db.run { - withInfo(creds) map (_.getOrElse(throw new RuntimeException(s"Cannot find appId for $creds"))) - } pipeTo self + withInfo(creds) map (_.getOrElse(throw new RuntimeException(s"Cannot find appId for $creds"))) pipeTo self - private def withInfo(c: PushCredentials): DBIO[Option[(PushCredentials, PushCredentialsInfo)]] = - for { + private def withInfo(c: PushCredentials): Future[Option[(PushCredentials, PushCredentialsInfo)]] = + db.run(for { authSessionOpt ← AuthSessionRepo.findByAuthId(c.authId) - } yield authSessionOpt map (s ⇒ c → PushCredentialsInfo(s.appId, c.authId)) + } yield authSessionOpt map (s ⇒ c → PushCredentialsInfo(s.appId, c.authId))) private def remove(creds: PushCredentials): Unit = mapping -= creds @@ -327,10 +390,11 @@ private[sequence] final class VendorPush(userId: Int) extends Actor with ActorLo val replyTo = sender() if (mapping.contains(creds)) { remove(creds) - val removeFu = db.run(creds match { - case c: GooglePushCredentials ⇒ GooglePushCredentialsRepo.deleteByToken(c.regId) - case c: ApplePushCredentials ⇒ ApplePushCredentialsRepo.deleteByToken(c.token.toByteArray) - case c: ActorPushCredentials ⇒ ActorPushCredentialsRepo.deleteByTopic(c.endpoint) + val removeFu = (creds match { + case c: GCMPushCredentials ⇒ db.run(GooglePushCredentialsRepo.deleteByToken(c.regId)) + case c: FirebasePushCredentials ⇒ firebaseKv.deleteByToken(c.regId) + case c: ApplePushCredentials ⇒ db.run(ApplePushCredentialsRepo.deleteByToken(c.token.toByteArray)) + case c: ActorPushCredentials ⇒ db.run(ActorPushCredentialsRepo.deleteByTopic(c.endpoint)) }) map (_ ⇒ UnregisterPushCredentialsAck()) pipeTo replyTo removeFu onFailure { diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/operations/DeliveryOperations.scala b/actor-server/actor-core/src/main/scala/im/actor/server/sequence/operations/DeliveryOperations.scala index 62a149c645..b4d7d30e53 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/operations/DeliveryOperations.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/sequence/operations/DeliveryOperations.scala @@ -1,9 +1,8 @@ package im.actor.server.sequence.operations import com.google.protobuf.ByteString -import com.google.protobuf.wrappers.StringValue import im.actor.api.rpc.Update -import im.actor.server.model.{ Peer, SerializedUpdate, UpdateMapping } +import im.actor.server.model.{ SerializedUpdate, UpdateMapping } import im.actor.server.sequence.UserSequenceCommands.{ DeliverUpdate, Envelope } import im.actor.server.sequence.{ PushData, PushRules, SeqState, SeqUpdatesExtension } import akka.pattern.ask @@ -13,20 +12,23 @@ import scala.concurrent.Future trait DeliveryOperations { this: SeqUpdatesExtension ⇒ def pushRules(isFat: Boolean, pushText: Option[String], excludeAuthIds: Seq[Long] = Seq.empty): PushRules = - PushRules(isFat = isFat) - .withData(PushData().withText(pushText.getOrElse(""))) - .withExcludeAuthIds(excludeAuthIds) + PushRules( + isFat = isFat, + excludeAuthIds = excludeAuthIds, + data = pushText map (t ⇒ PushData(text = t)) + ) /** * Send update to all devices of user and return `SeqState` associated with `authId` */ def deliverClientUpdate( - userId: Int, - authId: Long, - update: Update, - pushRules: PushRules = PushRules(), - reduceKey: Option[String] = None, - deliveryId: String = "" + userId: Int, + authId: Long, + update: Update, + pushRules: PushRules = PushRules(), + reduceKey: Option[String] = None, + deliveryId: String = "", + deliveryTag: Option[String] = None ): Future[SeqState] = deliverUpdate( userId, @@ -34,7 +36,8 @@ trait DeliveryOperations { this: SeqUpdatesExtension ⇒ UpdateMapping(default = Some(serializedUpdate(update))), pushRules, reduceKey, - deliveryId + deliveryId, + deliveryTag ) /** @@ -93,14 +96,15 @@ trait DeliveryOperations { this: SeqUpdatesExtension ⇒ * Send update to all devices of users from `userIds` set and return `Unit` */ def broadcastPeopleUpdate( - userIds: Set[Int], - update: Update, - pushRules: PushRules = PushRules(), - reduceKey: Option[String] = None, - deliveryId: String = "" + userIds: Set[Int], + update: Update, + pushRules: PushRules = PushRules(), + reduceKey: Option[String] = None, + deliveryId: String = "", + deliveryTag: Option[String] = None ): Future[Unit] = { val mapping = UpdateMapping(default = Some(serializedUpdate(update))) - val deliver = buildDeliver(0L, mapping, pushRules, reduceKey, deliveryId, deliveryTag = None) // TODO: add deliveryTag when needed + val deliver = buildDeliver(0L, mapping, pushRules, reduceKey, deliveryId, deliveryTag) broadcastUpdate(userIds, deliver) } @@ -145,13 +149,14 @@ trait DeliveryOperations { this: SeqUpdatesExtension ⇒ Future.sequence(userIds.toSeq map (deliverUpdate(_, deliver))) map (_ ⇒ ()) private def deliverUpdate(userId: Int, deliver: DeliverUpdate): Future[SeqState] = { - val isUpdateDefined = - deliver.getMapping.default.isDefined || deliver.getMapping.custom.nonEmpty - require(isUpdateDefined, "No default update nor authId-specific") + require( + deliver.getMapping.default.isDefined || deliver.getMapping.custom.nonEmpty, + "No default update nor authId-specific" + ) (region.ref ? Envelope(userId).withDeliverUpdate(deliver)).mapTo[SeqState] } - private def serializedUpdate(u: Update): SerializedUpdate = + protected def serializedUpdate(u: Update): SerializedUpdate = SerializedUpdate(u.header, ByteString.copyFrom(u.toByteArray), u._relatedUserIds, u._relatedGroupIds) private def buildDeliver( diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/operations/DifferenceOperations.scala b/actor-server/actor-core/src/main/scala/im/actor/server/sequence/operations/DifferenceOperations.scala index 430ab302e5..e89d7d457d 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/operations/DifferenceOperations.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/sequence/operations/DifferenceOperations.scala @@ -1,7 +1,9 @@ package im.actor.server.sequence.operations import akka.http.scaladsl.util.FastFuture +import im.actor.api.rpc.messaging._ import im.actor.api.rpc.sequence.UpdateEmptyUpdate +import im.actor.server.dialog.{ DialogGroupKeys, DialogGroupTitles } import im.actor.server.model.{ SeqUpdate, SerializedUpdate, UpdateMapping } import im.actor.server.persist.sequence.UserSequenceRepo import im.actor.server.sequence.{ CommonState, Difference, SeqUpdatesExtension } @@ -18,7 +20,12 @@ trait DifferenceOperations { this: SeqUpdatesExtension ⇒ private type ReduceKey = String private object DiffAcc { - def empty(commonSeq: Int) = DiffAcc(commonSeq, 0, immutable.TreeMap.empty, Map.empty) + def empty(commonSeq: Int) = DiffAcc( + commonSeq = commonSeq, + seqDelta = 0, + generic = immutable.TreeMap.empty, + reduced = Map.empty + ) } /** @@ -42,7 +49,95 @@ trait DifferenceOperations { this: SeqUpdatesExtension ⇒ ) { def nonEmpty = generic.nonEmpty || reduced.nonEmpty - def toVector = (generic ++ reduced.values).values.toVector + private def rewriteDialogsCounter(dialogs: IndexedSeq[ApiDialogShort], upd: UpdateMessageReadByMe) = + dialogs map { dlg ⇒ + if (upd.peer == dlg.peer) { + dlg.copy(counter = upd.unreadCounter getOrElse 0) + } else { + dlg + } + } + + private def incrementCounter(dialogs: IndexedSeq[ApiDialogShort], upd: UpdateMessage) = + dialogs map { dlg ⇒ + if (upd.peer == dlg.peer && upd.date > dlg.date) { + dlg.copy(counter = dlg.counter + 1) + } else { + dlg + } + } + + def toVector = { + val originalUpdates = (generic ++ reduced.values).values.toVector + + // If there is UpdateChatGroupsChanged in difference, + // we are writing up to date counters from UpdateMessageReadByMe + // for each peer we have in UpdateMessageReadByMe updates. + // After that, we update original difference + val optLastChatsChanged: Option[(Int, UpdateChatGroupsChanged)] = + originalUpdates.zipWithIndex.reverse find { + case (upd, i) ⇒ + upd.header == UpdateChatGroupsChanged.header + } flatMap { + case (upd, i) ⇒ + UpdateChatGroupsChanged.parseFrom(upd.body).right.toOption map (i → _) + } + + optLastChatsChanged match { + case None ⇒ originalUpdates + case Some((index, chatsChanged)) ⇒ + def singleGroup(groupKey: String) = (chatsChanged.dialogs collect { + case group if group.key == groupKey ⇒ group.dialogs + }).flatten + + val (favourites, groups, direct) = (originalUpdates foldLeft ( + singleGroup(DialogGroupKeys.Favourites), + singleGroup(DialogGroupKeys.Groups), + singleGroup(DialogGroupKeys.Direct) + )) { + case (acc @ (fav, gr, dir), upd) ⇒ + if (upd.header == UpdateMessageReadByMe.header) { + UpdateMessageReadByMe.parseFrom(upd.body).right.toOption map { upd ⇒ + ( + rewriteDialogsCounter(fav, upd), + rewriteDialogsCounter(gr, upd), + rewriteDialogsCounter(dir, upd) + ) + } getOrElse acc + } else if (upd.header == upd.header) { + UpdateMessage.parseFrom(upd.body).right.toOption map { upd ⇒ + ( + incrementCounter(fav, upd), + incrementCounter(gr, upd), + incrementCounter(dir, upd) + ) + } getOrElse acc + } else acc + } + + val chatsChangedUpdated = + chatsChanged.copy( + Vector( + ApiDialogGroup( + title = DialogGroupTitles.Favourites, + key = DialogGroupKeys.Favourites, + dialogs = favourites + ), + ApiDialogGroup( + title = DialogGroupTitles.Groups, + key = DialogGroupKeys.Groups, + dialogs = groups + ), + ApiDialogGroup( + title = DialogGroupTitles.Direct, + key = DialogGroupKeys.Direct, + dialogs = direct + ) + ) + ) + originalUpdates.updated(index, serializedUpdate(chatsChangedUpdated)) + } + } } def getDifference( diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/operations/PushOperations.scala b/actor-server/actor-core/src/main/scala/im/actor/server/sequence/operations/PushOperations.scala index ec913292cb..f394b898ed 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/sequence/operations/PushOperations.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/sequence/operations/PushOperations.scala @@ -2,21 +2,23 @@ package im.actor.server.sequence.operations import akka.http.scaladsl.util.FastFuture import im.actor.server.model.AuthSession -import im.actor.server.model.push.{ ActorPushCredentials, ApplePushCredentials, GooglePushCredentials, PushCredentials } +import im.actor.server.model.push._ import im.actor.server.persist.AuthSessionRepo import im.actor.server.persist.push.{ ActorPushCredentialsRepo, ApplePushCredentialsRepo, GooglePushCredentialsRepo } import im.actor.server.sequence.SeqUpdatesExtension import im.actor.server.sequence.UserSequenceCommands.{ Envelope, RegisterPushCredentials, UnregisterPushCredentials, UnregisterPushCredentialsAck } import scodec.bits.BitVector - import akka.pattern.ask import scala.concurrent.Future trait PushOperations { this: SeqUpdatesExtension ⇒ - def registerGooglePushCredentials(creds: GooglePushCredentials) = - registerPushCredentials(creds.authId, RegisterPushCredentials().withGoogle(creds)) + def registerGCMPushCredentials(creds: GCMPushCredentials) = + registerPushCredentials(creds.authId, RegisterPushCredentials().withGcm(creds)) + + def registerFirebasePushCredentials(creds: FirebasePushCredentials) = + registerPushCredentials(creds.authId, RegisterPushCredentials().withFirebase(creds)) def registerApplePushCredentials(creds: ApplePushCredentials) = registerPushCredentials(creds.authId, RegisterPushCredentials().withApple(creds)) @@ -54,10 +56,19 @@ trait PushOperations { this: SeqUpdatesExtension ⇒ FastFuture.successful(()) } - def unregisterGooglePushCredentials(token: String): Future[Unit] = + def unregisterGCMPushCredentials(token: String): Future[Unit] = db.run(GooglePushCredentialsRepo.findByToken(token)) flatMap { case Some(creds) ⇒ - unregisterPushCredentials(creds.authId, UnregisterPushCredentials().withGoogle(creds)) + unregisterPushCredentials(creds.authId, UnregisterPushCredentials().withGcm(creds)) + case None ⇒ + log.warning("Google push credentials not found for token: {}", token) + FastFuture.successful(()) + } + + def unregisterFirebasePushCredentials(token: String): Future[Unit] = + firebaseKv.findByToken(token) flatMap { + case Some(creds) ⇒ + unregisterPushCredentials(creds.authId, UnregisterPushCredentials().withFirebase(creds)) case None ⇒ log.warning("Google push credentials not found for token: {}", token) FastFuture.successful(()) @@ -69,9 +80,10 @@ trait PushOperations { this: SeqUpdatesExtension ⇒ } private def makeUnregister: PartialFunction[PushCredentials, UnregisterPushCredentials] = { - case actor: ActorPushCredentials ⇒ UnregisterPushCredentials().withActor(actor) - case apple: ApplePushCredentials ⇒ UnregisterPushCredentials().withApple(apple) - case google: GooglePushCredentials ⇒ UnregisterPushCredentials().withGoogle(google) + case actor: ActorPushCredentials ⇒ UnregisterPushCredentials().withActor(actor) + case apple: ApplePushCredentials ⇒ UnregisterPushCredentials().withApple(apple) + case gcm: GCMPushCredentials ⇒ UnregisterPushCredentials().withGcm(gcm) + case firebase: FirebasePushCredentials ⇒ UnregisterPushCredentials().withFirebase(firebase) } private def findAllPushCredentials(authId: Long): Future[Seq[PushCredentials]] = diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/user/UserCommandHandlers.scala b/actor-server/actor-core/src/main/scala/im/actor/server/user/UserCommandHandlers.scala index e6d1cccdde..b7204ee7fb 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/user/UserCommandHandlers.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/user/UserCommandHandlers.scala @@ -1,6 +1,6 @@ package im.actor.server.user -import java.time.{ LocalDateTime, ZoneOffset } +import java.time.{ Instant, LocalDateTime, ZoneOffset } import java.util.TimeZone import akka.actor.{ ActorSystem, Status } @@ -19,6 +19,7 @@ import im.actor.server.bots.BotCommand import im.actor.server.file.{ Avatar, ImageUtils } import im.actor.server.model.{ AvatarData, Sex, User } import im.actor.server.model.contact.{ UserContact, UserEmailContact, UserPhoneContact } +import im.actor.server.names.{ GlobalNameOwner, OwnerType } import im.actor.server.office.EntityNotFound import im.actor.server.persist.contact._ import im.actor.server.persist._ @@ -217,12 +218,12 @@ private[user] trait UserCommandHandlers { onSuccess(checkNicknameExists(nicknameOpt)) { exists ⇒ if (!exists) { - if (nicknameOpt forall StringUtils.validUsername) { + if (nicknameOpt forall StringUtils.validGlobalName) { persistReply(UserEvents.NicknameChanged(now(), nicknameOpt), user, replyTo) { _ ⇒ val update = UpdateUserNickChanged(userId, nicknameOpt) for { - _ ← db.run(UserRepo.setNickname(userId, nicknameOpt)) + _ ← globalNamesStorage.updateOrRemove(user.nickname, nicknameOpt, GlobalNameOwner(OwnerType.User, userId)) relatedUserIds ← getRelations(userId) seqState ← seqUpdExt.broadcastClientUpdate(userId, authId, relatedUserIds, update) } yield seqState @@ -429,20 +430,20 @@ private[user] trait UserCommandHandlers { private def checkNicknameExists(nicknameOpt: Option[String]): Future[Boolean] = { nicknameOpt match { - case Some(nickname) ⇒ db.run(UserRepo.nicknameExists(nickname)) + case Some(nickname) ⇒ globalNamesStorage.exists(nickname) case None ⇒ FastFuture.successful(false) } } // TODO: DRY it, finally! private def markContactRegistered(user: UserState, phoneNumber: Long, isSilent: Boolean): Future[Unit] = { - val date = new DateTime + val dateMillis = Instant.now.toEpochMilli for { contacts ← db.run(UnregisteredPhoneContactRepo.find(phoneNumber)) _ = log.debug(s"Unregistered $phoneNumber is in contacts of users: $contacts") _ ← Future.sequence(contacts map { contact ⇒ val randomId = ThreadLocalSecureRandom.current().nextLong() - val updateContactRegistered = UpdateContactRegistered(user.id, isSilent, date.getMillis, randomId) + val updateContactRegistered = UpdateContactRegistered(user.id, isSilent, dateMillis, randomId) val updateContactsAdded = UpdateContactsAdded(Vector(user.id)) val localName = contact.name val serviceMessage = ServiceMessages.contactRegistered(user.id, localName.getOrElse(user.name)) @@ -462,7 +463,7 @@ private[user] trait UserCommandHandlers { contact.ownerUserId, ApiPeer(ApiPeerType.Private, user.id), user.id, - date, + dateMillis, randomId, serviceMessage ) @@ -475,14 +476,14 @@ private[user] trait UserCommandHandlers { } private def markContactRegistered(user: UserState, email: String, isSilent: Boolean): Future[Unit] = { - val date = new DateTime + val dateMillis = Instant.now.toEpochMilli for { _ ← userExt.hooks.beforeEmailContactRegistered.runAll(user.id, email) contacts ← db.run(UnregisteredEmailContactRepo.find(email)) _ = log.debug(s"Unregistered $email is in contacts of users: $contacts") _ ← Future.sequence(contacts.map { contact ⇒ val randomId = ThreadLocalSecureRandom.current().nextLong() - val updateContactRegistered = UpdateContactRegistered(user.id, isSilent, date.getMillis, randomId) + val updateContactRegistered = UpdateContactRegistered(user.id, isSilent, dateMillis, randomId) val updateContactsAdded = UpdateContactsAdded(Vector(user.id)) val localName = contact.name val serviceMessage = ServiceMessages.contactRegistered(user.id, localName.getOrElse(user.name)) @@ -502,7 +503,7 @@ private[user] trait UserCommandHandlers { contact.ownerUserId, ApiPeer(ApiPeerType.Private, user.id), user.id, - date, + dateMillis, randomId, serviceMessage ) diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/user/UserOperations.scala b/actor-server/actor-core/src/main/scala/im/actor/server/user/UserOperations.scala index a8ca2f0903..56cd6bb307 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/user/UserOperations.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/user/UserOperations.scala @@ -11,6 +11,7 @@ import im.actor.server.auth.DeviceInfo import im.actor.server.bots.BotCommand import im.actor.server.db.DbExtension import im.actor.server.file.Avatar +import im.actor.server.names.GlobalNamesStorageKeyValueStorage import im.actor.server.persist.UserRepo import im.actor.server.pubsub.PubSubExtension import im.actor.server.sequence.{ SeqState, SeqUpdatesExtension } @@ -144,6 +145,7 @@ private[user] sealed trait Queries { implicit val system: ActorSystem import system.dispatcher val log: LoggingAdapter + private lazy val globalNamesStorage = new GlobalNamesStorageKeyValueStorage implicit val timeout: Timeout @@ -192,7 +194,11 @@ private[user] sealed trait Queries { def isAdmin(userId: Int): Future[Boolean] = (viewRegion.ref ? IsAdmin(userId)).mapTo[IsAdminResponse].map(_.isAdmin) - def findUserIds(query: String): Future[Seq[Int]] = DbExtension(system).db.run(UserRepo.findIds(query)) + def findUserIds(query: String): Future[Seq[Int]] = + for { + byPhoneAndEmail ← DbExtension(system).db.run(UserRepo.findIds(query)) + byNickname ← globalNamesStorage.getUserId(query) + } yield byPhoneAndEmail ++ byNickname } private[user] sealed trait AuthCommands { diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/user/UserProcessor.scala b/actor-server/actor-core/src/main/scala/im/actor/server/user/UserProcessor.scala index a9269035ef..d3f97ff138 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/user/UserProcessor.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/user/UserProcessor.scala @@ -15,6 +15,7 @@ import im.actor.server.cqrs.TaggedEvent import im.actor.server.db.DbExtension import im.actor.server.dialog._ import im.actor.server.model.{ Peer, PeerType } +import im.actor.server.names.GlobalNamesStorageKeyValueStorage import im.actor.server.office.{ PeerProcessor, StopOffice } import im.actor.server.sequence.SeqUpdatesExtension import im.actor.server.social.{ SocialExtension, SocialManagerRegion } @@ -161,6 +162,7 @@ private[user] final class UserProcessor protected lazy val dialogExt = DialogExtension(system) protected val seqUpdExt: SeqUpdatesExtension = SeqUpdatesExtension(system) protected implicit val socialRegion: SocialManagerRegion = SocialExtension(system).region + protected val globalNamesStorage = new GlobalNamesStorageKeyValueStorage protected implicit val timeout: Timeout = Timeout(10.seconds) @@ -252,17 +254,14 @@ private[user] final class UserProcessor case query: GetLocalName ⇒ contacts.ref forward query case StopOffice ⇒ context stop self case ReceiveTimeout ⇒ context.parent ! ShardRegion.Passivate(stopMessage = StopOffice) - case e @ DialogRootEnvelope(query, command) ⇒ - val msg = e.getAllFields.values.head + case env: DialogRootEnvelope ⇒ + val msg = env.getAllFields.values.head (dialogRoot(state.internalExtensions) ? msg) pipeTo sender() case de: DialogEnvelope ⇒ val msg = de.getAllFields.values.head msg match { - case dc: DialogCommand if dc.isInstanceOf[DialogCommands.SendMessage] || dc.isInstanceOf[DialogCommands.WriteMessageSelf] ⇒ - dialogRoot(state.internalExtensions) ! msg - handleDialogCommand(state)(dc) case dc: DialogCommand ⇒ handleDialogCommand(state)(dc) case dq: DialogQuery ⇒ handleDialogQuery(state)(dq) } diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/userconfig/SettingsKeys.scala b/actor-server/actor-core/src/main/scala/im/actor/server/userconfig/SettingsKeys.scala index 0f800020fc..a30578fa45 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/userconfig/SettingsKeys.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/userconfig/SettingsKeys.scala @@ -1,7 +1,5 @@ package im.actor.server.userconfig -import im.actor.server.model.{ Peer, PeerType } - object SettingsKeys { private def wrap(deviceType: String, postfix: String): String = s"category.$deviceType.notification.$postfix" @@ -9,6 +7,8 @@ object SettingsKeys { private def wrapEnabled(deviceType: String, postfix: String): String = s"category.$deviceType.notification.$postfix.enabled" + private def groups(postfix: String) = s"account.notifications.group.$postfix" + def enabled(deviceType: String) = wrapEnabled(deviceType) def soundEnabled(deviceType: String) = wrapEnabled(deviceType, "sound") @@ -16,4 +16,9 @@ object SettingsKeys { def vibrationEnabled(deviceType: String) = wrapEnabled(deviceType, "vibration") def textEnabled(deviceType: String) = wrap(deviceType, "show_text") + + def accountGroupEnabled = groups("enabled") + + def accountGroupMentionEnabled = groups("mentions") + } diff --git a/actor-server/actor-core/src/main/scala/im/actor/server/webrtc/WebrtcCallActor.scala b/actor-server/actor-core/src/main/scala/im/actor/server/webrtc/WebrtcCallActor.scala index 3778eb92eb..57e676238d 100644 --- a/actor-server/actor-core/src/main/scala/im/actor/server/webrtc/WebrtcCallActor.scala +++ b/actor-server/actor-core/src/main/scala/im/actor/server/webrtc/WebrtcCallActor.scala @@ -14,6 +14,8 @@ import im.actor.server.eventbus.{ EventBus, EventBusExtension } import im.actor.server.group.GroupExtension import im.actor.server.model.{ Peer, PeerType } import im.actor.server.push.actor.{ ActorPush, ActorPushMessage } +import im.actor.server.push.apple.{ APNSSend, ApplePushExtension } +import im.actor.server.push.google.{ FirebasePushExtension, GCMPushExtension, GooglePushMessage } import im.actor.server.sequence._ import im.actor.server.user.UserExtension import im.actor.server.values.ValuesExtension @@ -170,7 +172,8 @@ private final class WebrtcCallActor extends StashingActor with ActorLogging with private val groupExt = GroupExtension(system) private val valuesExt = ValuesExtension(system) private val apnsExt = ApplePushExtension(system) - private val gcmExt = GooglePushExtension(system) + private val gcmExt = GCMPushExtension(system) + private val firebaseExt = FirebasePushExtension(system) private val actorPush = ActorPush(system) private val webrtcExt = WebrtcExtension(system) @@ -544,22 +547,26 @@ private final class WebrtcCallActor extends StashingActor with ActorLogging with private def scheduleIncomingCallUpdates(callees: Seq[UserId]): Future[Unit] = { val pushCredsFu = for { authIdsMap ← userExt.getAuthIdsMap(callees.toSet) - acredsMap ← FutureExt.ftraverse(authIdsMap.toSeq) { + appleCredsMap ← FutureExt.ftraverse(authIdsMap.toSeq) { case (userId, authIds) ⇒ apnsExt.fetchVoipCreds(authIds.toSet) map (userId → _) } - gcredsMap ← FutureExt.ftraverse(authIdsMap.toSeq) { + gcmCredsMap ← FutureExt.ftraverse(authIdsMap.toSeq) { case (userId, authIds) ⇒ gcmExt.fetchCreds(authIds.toSet) map (userId → _) } + firebaseCredsMap ← FutureExt.ftraverse(authIdsMap.toSeq) { + case (userId, authIds) ⇒ + firebaseExt.fetchCreds(authIds.toSet) map (userId → _) + } actorCredsMap ← FutureExt.ftraverse(authIdsMap.toSeq) { case (userId, authIds) ⇒ actorPush.fetchCreds(userId) map (userId → _) } - } yield (acredsMap, gcredsMap, actorCredsMap) + } yield (appleCredsMap, gcmCredsMap, firebaseCredsMap, actorCredsMap) pushCredsFu map { - case (appleCreds, googleCreds, actorCreds) ⇒ + case (appleCreds, gcmCreds, firebaseCreds, actorCreds) ⇒ for { (userId, credsList) ← appleCreds creds ← credsList @@ -572,18 +579,27 @@ private final class WebrtcCallActor extends StashingActor with ActorLogging with _ = clientFu foreach { implicit c ⇒ sendNotification(payload, creds, userId) } } yield () + def googlePushMessage(regId: String) = GooglePushMessage( + regId, + None, + Some(Map("callId" → id.toString, "attemptIndex" → "1")), + time_to_live = Some(0) + ) + for { - (member, creds) ← googleCreds + (member, creds) ← gcmCreds cred ← creds - message = new GooglePushMessage( - cred.regId, - None, - Some(Map("callId" → id.toString, "attemptIndex" → "1")), - time_to_live = Some(0) - ) + message = googlePushMessage(cred.regId) _ = gcmExt.send(cred.projectId, message) } yield () + for { + (member, creds) ← firebaseCreds + cred ← creds + message = googlePushMessage(cred.regId) + _ = firebaseExt.send(cred.projectId, message) + } yield () + for { (member, creds) ← actorCreds cred ← creds diff --git a/actor-server/actor-email/src/main/resources/reference.conf b/actor-server/actor-email/src/main/resources/reference.conf index 1edcf9a184..7972935b3e 100644 --- a/actor-server/actor-email/src/main/resources/reference.conf +++ b/actor-server/actor-email/src/main/resources/reference.conf @@ -10,9 +10,9 @@ services { host: "smtp.gmail.com" port: 465 username: "no-reply@actor.im" - username: ${?SMTP_USERNAME} + username: ${?ACTOR_SMTP_USERNAME} password: "pass" - password = ${?SMTP_PASSWORD} + password: ${?ACTOR_SMTP_PASSWORD} tls: true } } diff --git a/actor-server/actor-email/src/main/scala/im/actor/server/email/EmailExtension.scala b/actor-server/actor-email/src/main/scala/im/actor/server/email/EmailExtension.scala index eb95967a64..b58b77e2dd 100644 --- a/actor-server/actor-email/src/main/scala/im/actor/server/email/EmailExtension.scala +++ b/actor-server/actor-email/src/main/scala/im/actor/server/email/EmailExtension.scala @@ -6,7 +6,7 @@ sealed trait EmailExtension extends Extension final class EmailExtensionImpl(system: ActorSystem) extends EmailExtension { import system.dispatcher - private val config = EmailConfig.load.get + private val config = EmailConfig.load.getOrElse(throw new RuntimeException("Failed to load email config")) val sender: EmailSender = new SmtpEmailSender(config) } diff --git a/actor-server/actor-enrich/src/main/scala/im/actor/server/enrich/PreviewMaker.scala b/actor-server/actor-enrich/src/main/scala/im/actor/server/enrich/PreviewMaker.scala index 8062bb9de6..c5bf9dd624 100644 --- a/actor-server/actor-enrich/src/main/scala/im/actor/server/enrich/PreviewMaker.scala +++ b/actor-server/actor-enrich/src/main/scala/im/actor/server/enrich/PreviewMaker.scala @@ -7,12 +7,11 @@ import spray.http.HttpHeaders.`Content-Disposition` import spray.http.HttpMethods.GET import spray.http._ -import scala.concurrent.{ ExecutionContext, Future } +import scala.concurrent.Future object PreviewMaker { - def apply(config: RichMessageConfig, name: String)(implicit system: ActorSystem): ActorRef = - system.actorOf(Props(classOf[PreviewMaker], config), name) + def props(config: RichMessageConfig) = Props(classOf[PreviewMaker], config) object Failures { object Messages { @@ -66,4 +65,4 @@ class PreviewMaker(config: RichMessageConfig) extends Actor with ActorLogging wi result pipeTo sender() case _ ⇒ } -} \ No newline at end of file +} diff --git a/actor-server/actor-enrich/src/main/scala/im/actor/server/enrich/RichMessageWorker.scala b/actor-server/actor-enrich/src/main/scala/im/actor/server/enrich/RichMessageWorker.scala index b0ab3d83c1..457abb56cf 100644 --- a/actor-server/actor-enrich/src/main/scala/im/actor/server/enrich/RichMessageWorker.scala +++ b/actor-server/actor-enrich/src/main/scala/im/actor/server/enrich/RichMessageWorker.scala @@ -40,8 +40,6 @@ final class RichMessageWorker(config: RichMessageConfig) extends Actor with Acto override val log = Logging(system, this) - private val previewMaker = PreviewMaker(config, "previewMaker") - private val privateSubscribe = Subscribe(pubSubExt.privateMessagesTopic, groupId, self) private val publicSubscribe = Subscribe(pubSubExt.groupMessagesTopic, None, self) @@ -94,7 +92,7 @@ final class RichMessageWorker(config: RichMessageConfig) extends Actor with Acto updated = ApiDocumentMessage( fileId = location.fileId, accessHash = location.accessHash, - fileSize = imageBytes.size, + fileSize = imageBytes.length, name = fullName, mimeType = mimeType, thumb = Some(ApiFastThumb(thumb.width, thumb.height, thumbBytes)), @@ -106,4 +104,9 @@ final class RichMessageWorker(config: RichMessageConfig) extends Actor with Acto log.debug("failed to make preview for message with randomId: {}, cause: {} ", randomId, mess) } + private def previewMaker: ActorRef = { + val name = "preview-maker" + context.child(name).getOrElse(context.actorOf(PreviewMaker.props(config), name)) + } + } diff --git a/actor-server/actor-frontend/src/main/scala/im/actor/server/frontend/MTProtoBlueprint.scala b/actor-server/actor-frontend/src/main/scala/im/actor/server/frontend/MTProtoBlueprint.scala index 19f20eb8fc..a34a5ab998 100644 --- a/actor-server/actor-frontend/src/main/scala/im/actor/server/frontend/MTProtoBlueprint.scala +++ b/actor-server/actor-frontend/src/main/scala/im/actor/server/frontend/MTProtoBlueprint.scala @@ -3,6 +3,7 @@ package im.actor.server.frontend import java.net.InetAddress import akka.stream.FlowShape +import com.github.ghik.silencer.silent import kamon.metric.instrument.{ MinMaxCounter, Histogram } import scala.util.{ Failure, Success } @@ -32,11 +33,11 @@ object MTProtoBlueprint { val sessionClientSource = Source.fromPublisher(ActorPublisher[MTProto](sessionClient)) - val mtprotoFlow = Flow.fromGraph(new PackageParseStage()) + @silent val mtprotoFlow = Flow.fromGraph(new PackageParseStage()) .transform(() ⇒ new PackageCheckStage) .via(new PackageHandleStage(protoVersions, apiMajorVersions, authManager, sessionClient)) - val mapRespFlow: Flow[MTProto, ByteString, akka.NotUsed] = Flow[MTProto] + @silent val mapRespFlow: Flow[MTProto, ByteString, akka.NotUsed] = Flow[MTProto] .transform(() ⇒ mapResponse(system)) val connStartTime = System.currentTimeMillis() @@ -82,7 +83,7 @@ object MTProtoBlueprint { }) } - def mapResponse(system: ActorSystem) = new PushStage[MTProto, ByteString] { + @silent def mapResponse(system: ActorSystem) = new PushStage[MTProto, ByteString] { private[this] var packageIndex: Int = -1 override def onPush(elem: MTProto, ctx: Context[ByteString]) = { diff --git a/actor-server/actor-frontend/src/main/scala/im/actor/server/frontend/PackageCheckStage.scala b/actor-server/actor-frontend/src/main/scala/im/actor/server/frontend/PackageCheckStage.scala index 489f13041c..882965594b 100644 --- a/actor-server/actor-frontend/src/main/scala/im/actor/server/frontend/PackageCheckStage.scala +++ b/actor-server/actor-frontend/src/main/scala/im/actor/server/frontend/PackageCheckStage.scala @@ -1,9 +1,10 @@ package im.actor.server.frontend import akka.stream.stage.{ Context, PushStage, SyncDirective } - +import com.github.ghik.silencer.silent import im.actor.server.mtproto.transport.{ Handshake, TransportPackage } +@silent private[frontend] final class PackageCheckStage extends PushStage[TransportPackage, TransportPackage] { private trait State diff --git a/actor-server/actor-frontend/src/main/scala/im/actor/server/frontend/WsFrontend.scala b/actor-server/actor-frontend/src/main/scala/im/actor/server/frontend/WsFrontend.scala index 0f62209df4..8849da19a1 100644 --- a/actor-server/actor-frontend/src/main/scala/im/actor/server/frontend/WsFrontend.scala +++ b/actor-server/actor-frontend/src/main/scala/im/actor/server/frontend/WsFrontend.scala @@ -10,6 +10,7 @@ import akka.stream.{ ActorMaterializer, Materializer } import akka.stream.scaladsl._ import akka.stream.stage.{ Context, PushStage, SyncDirective, TerminationDirective } import akka.util.ByteString +import com.github.ghik.silencer.silent import im.actor.server.session.SessionRegion import scala.concurrent.duration._ @@ -66,6 +67,7 @@ object WsFrontend extends Frontend("ws") { .via(completionFlow(System.currentTimeMillis())) } + @silent def completionFlow[T](connStartTime: Long)(implicit system: ActorSystem): Flow[T, T, akka.NotUsed] = Flow[T] .transform(() ⇒ new PushStage[T, T] { diff --git a/actor-server/actor-fs-adapters/src/main/resources/reference.conf b/actor-server/actor-fs-adapters/src/main/resources/reference.conf index 468bce1ea7..cadaf8bfff 100644 --- a/actor-server/actor-fs-adapters/src/main/resources/reference.conf +++ b/actor-server/actor-fs-adapters/src/main/resources/reference.conf @@ -7,10 +7,8 @@ services { aws { s3 { - # provide your custom S3 endpoint if needed - endpoint: "" # enable S3 URLs with path-style access if needed path-style-access: false } } -} \ No newline at end of file +} diff --git a/actor-server/actor-fs-adapters/src/main/scala/im/actor/server/file/local/FileStorageOperations.scala b/actor-server/actor-fs-adapters/src/main/scala/im/actor/server/file/local/FileStorageOperations.scala index 7ffe56f466..c1d593176a 100644 --- a/actor-server/actor-fs-adapters/src/main/scala/im/actor/server/file/local/FileStorageOperations.scala +++ b/actor-server/actor-fs-adapters/src/main/scala/im/actor/server/file/local/FileStorageOperations.scala @@ -10,6 +10,7 @@ import akka.stream.scaladsl.{ FileIO, Source } import akka.util.ByteString import better.files.{ File, _ } import im.actor.server.db.DbExtension +import im.actor.server.file.UnsafeFileName import im.actor.server.persist.files.FileRepo import scala.concurrent.{ ExecutionContext, Future, blocking } @@ -116,7 +117,7 @@ trait FileStorageOperations extends LocalUploadKeyImplicits { protected def getFileData(file: File): Future[ByteString] = FileIO.fromPath(file.path).runFold(ByteString.empty)(_ ++ _) - protected def getFileName(name: String) = if (name.trim.isEmpty) "file" else name + protected def getFileName(name: String) = if (name.trim.isEmpty) "file" else UnsafeFileName(name).safe protected def fileDirectory(fileId: Long): File = file"$storageLocation/file_${fileId}" diff --git a/actor-server/actor-fs-adapters/src/main/scala/im/actor/server/file/s3/S3StorageAdapter.scala b/actor-server/actor-fs-adapters/src/main/scala/im/actor/server/file/s3/S3StorageAdapter.scala index 653228bd0d..93773a3670 100644 --- a/actor-server/actor-fs-adapters/src/main/scala/im/actor/server/file/s3/S3StorageAdapter.scala +++ b/actor-server/actor-fs-adapters/src/main/scala/im/actor/server/file/s3/S3StorageAdapter.scala @@ -9,6 +9,7 @@ import akka.stream.{ ActorMaterializer, Materializer } import akka.util.ByteString import com.amazonaws.HttpMethod import com.amazonaws.auth.BasicAWSCredentials +import com.amazonaws.regions.{ Region, Regions } import com.amazonaws.services.s3.model.{ GeneratePresignedUrlRequest, ObjectMetadata } import com.amazonaws.services.s3.S3ClientOptions import com.amazonaws.services.s3.transfer.TransferManager @@ -32,21 +33,32 @@ final class S3StorageAdapter(_system: ActorSystem) extends FileStorageAdapter { private implicit val ec: ExecutionContext = system.dispatcher private implicit val mat: Materializer = ActorMaterializer() - private val config = S3StorageAdapterConfig.load(system.settings.config.getConfig("services.aws.s3")).get - private val bucketName = config.bucketName - private val awsCredentials = new BasicAWSCredentials(config.key, config.secret) private val db = DbExtension(system).db - val s3Client = new AmazonS3ScalaClient(awsCredentials) - if (!config.endpoint.isEmpty) { - s3Client.client.setEndpoint(config.endpoint) + private val config = S3StorageAdapterConfig.load + private val bucketName = config.bucketName + private val awsCredentials = new BasicAWSCredentials(config.key, config.secret) + private val s3Client = { + val cl = new AmazonS3ScalaClient(awsCredentials) + config.region foreach { region ⇒ + cl.client.setRegion(Region.getRegion(Regions.fromName(region))) + } + config.endpoint foreach { endpoint ⇒ + cl.client.setEndpoint(endpoint) + } if (config.pathStyleAccess) { - s3Client.client.setS3ClientOptions(new S3ClientOptions().withPathStyleAccess(true)); + val co = S3ClientOptions.builder() + .setPathStyleAccess(true) + .setPayloadSigningEnabled(true) + .build() + cl.client.setS3ClientOptions(co) } + System.setProperty("com.amazonaws.services.s3.disableGetObjectMD5Validation", "true") + cl } - val transferManager = new TransferManager(s3Client.client) + private val transferManager = new TransferManager(s3Client.client) override def uploadFile(name: UnsafeFileName, data: Array[Byte]): DBIO[FileLocation] = uploadFile(bucketName, name, data) @@ -116,7 +128,6 @@ final class S3StorageAdapter(_system: ActorSystem) extends FileStorageAdapter { expiration.setTime(expiration.getTime + 1.day.toMillis) request.setMethod(HttpMethod.PUT) request.setExpiration(expiration) - request.setContentType("application/octet-stream") for (url ← s3Client.generatePresignedUrlRequest(request)) yield partKey → url.toString } @@ -135,10 +146,10 @@ final class S3StorageAdapter(_system: ActorSystem) extends FileStorageAdapter { override def completeFileUpload(fileId: Long, fileSize: Long, fileName: UnsafeFileName, partNames: Seq[String]): Future[Unit] = { for { tempDir ← createTempDir() - fk = uploadKey(fileId).key - _ ← FutureTransfer.listenFor { - transferManager.downloadDirectory(bucketName, s"upload_part_$fk", tempDir) - } map (_.waitForCompletion()) + _ ← Future.sequence(partNames map { part ⇒ + val path = tempDir.toPath.resolve(part) + FutureTransfer.listenFor(transferManager.download(bucketName, part, path.toFile)).map(_.waitForCompletion()) + }) concatFile ← concatFiles(tempDir, partNames) _ ← FutureTransfer.listenFor { transferManager.upload(bucketName, s3Key(fileId, fileName.safe), concatFile) diff --git a/actor-server/actor-fs-adapters/src/main/scala/im/actor/server/file/s3/S3StorageAdapterConfig.scala b/actor-server/actor-fs-adapters/src/main/scala/im/actor/server/file/s3/S3StorageAdapterConfig.scala index 6d5f06b2c7..751fee7602 100644 --- a/actor-server/actor-fs-adapters/src/main/scala/im/actor/server/file/s3/S3StorageAdapterConfig.scala +++ b/actor-server/actor-fs-adapters/src/main/scala/im/actor/server/file/s3/S3StorageAdapterConfig.scala @@ -3,23 +3,25 @@ package im.actor.server.file.s3 import com.github.kxbmap.configs.syntax._ import com.typesafe.config.{ ConfigFactory, Config } -import scala.util.Try +private[s3] case class S3StorageAdapterConfig( + bucketName: String, + region: Option[String], + key: String, + secret: String, + endpoint: Option[String], + pathStyleAccess: Boolean +) -case class S3StorageAdapterConfig(bucketName: String, key: String, secret: String, endpoint: String, pathStyleAccess: Boolean) - -object S3StorageAdapterConfig { - def load(config: Config): Try[S3StorageAdapterConfig] = { - for { - bucketName ← config.get[Try[String]]("default-bucket") - key ← config.get[Try[String]]("access-key") - secret ← config.get[Try[String]]("secret-key") - endpoint ← config.get[Try[String]]("endpoint") - pathStyleAccess ← config.get[Try[Boolean]]("path-style-access") - } yield S3StorageAdapterConfig(bucketName, key, secret, endpoint, pathStyleAccess) - } - - def load: Try[S3StorageAdapterConfig] = { - val config = ConfigFactory.load().getConfig("services.aws.s3") - load(config) +private[s3] object S3StorageAdapterConfig { + def load(config: Config): S3StorageAdapterConfig = { + S3StorageAdapterConfig( + bucketName = config.get[String]("default-bucket"), + region = config.getOpt[String]("region"), + key = config.get[String]("access-key"), + secret = config.get[String]("secret-key"), + endpoint = config.getOpt[String]("endpoint"), + pathStyleAccess = config.get[Boolean]("path-style-access") + ) } + def load: S3StorageAdapterConfig = load(ConfigFactory.load().getConfig("services.aws.s3")) } diff --git a/actor-server/actor-http-api/src/main/scala/im/actor/server/api/http/json/JsonEncoders.scala b/actor-server/actor-http-api/src/main/scala/im/actor/server/api/http/json/JsonEncoders.scala index fbf91168d5..10f109409e 100644 --- a/actor-server/actor-http-api/src/main/scala/im/actor/server/api/http/json/JsonEncoders.scala +++ b/actor-server/actor-http-api/src/main/scala/im/actor/server/api/http/json/JsonEncoders.scala @@ -4,5 +4,5 @@ import io.circe._ import io.circe.generic.semiauto._ trait JsonEncoders { - implicit val serverInfoFormat = deriveFor[ServerInfo].encoder + implicit val serverInfoFormat = deriveEncoder[ServerInfo] } diff --git a/actor-server/actor-http-api/src/main/scala/im/actor/server/api/http/json/JsonFormatters.scala b/actor-server/actor-http-api/src/main/scala/im/actor/server/api/http/json/JsonFormatters.scala index 348aaa1f84..1c203836de 100644 --- a/actor-server/actor-http-api/src/main/scala/im/actor/server/api/http/json/JsonFormatters.scala +++ b/actor-server/actor-http-api/src/main/scala/im/actor/server/api/http/json/JsonFormatters.scala @@ -11,8 +11,8 @@ object JsonFormatters { (JsPath \ "image_url").read[String].map[Content] { Image } implicit val avatarUrlsFormat: Format[AvatarUrls] = Json.format[AvatarUrls] - implicit val userFormat: Format[User] = Json.format[User] - implicit val groupFormat: Format[Group] = Json.format[Group] + implicit val userFormat: Format[InviterInfo] = Json.format[InviterInfo] + implicit val groupFormat: Format[GroupInfo] = Json.format[GroupInfo] implicit val groupInviteInfoFormat: Format[GroupInviteInfo] = Json.format[GroupInviteInfo] implicit val errorsFormat: Format[Errors] = Json.format[Errors] @@ -22,4 +22,4 @@ object JsonFormatters { implicit val reverseHookResponseFormat: Format[ReverseHookResponse] = Json.format[ReverseHookResponse] implicit val statusFormat: Format[Status] = Json.format[Status] -} \ No newline at end of file +} diff --git a/actor-server/actor-http-api/src/main/scala/im/actor/server/api/http/json/models.scala b/actor-server/actor-http-api/src/main/scala/im/actor/server/api/http/json/models.scala index 2758eba68a..b3ae4a1c1c 100644 --- a/actor-server/actor-http-api/src/main/scala/im/actor/server/api/http/json/models.scala +++ b/actor-server/actor-http-api/src/main/scala/im/actor/server/api/http/json/models.scala @@ -5,9 +5,9 @@ case class Text(text: String) extends Content case class Image(imageUrl: String) extends Content case class Document(documentUrl: String) extends Content -case class Group(title: String, avatars: Option[AvatarUrls]) -case class User(name: String, avatars: Option[AvatarUrls]) -case class GroupInviteInfo(group: Group, inviter: User) +case class GroupInfo(id: Int, title: String, isPublic: Boolean, avatars: Option[AvatarUrls]) +case class InviterInfo(name: String, avatars: Option[AvatarUrls]) +case class GroupInviteInfo(group: GroupInfo, inviter: Option[InviterInfo]) case class AvatarUrls(small: Option[String], large: Option[String], full: Option[String]) case class Errors(message: String) @@ -17,4 +17,4 @@ case class ReverseHook(url: String) case class Status(status: String) case class ReverseHookResponse(id: Int, url: Option[String]) -final case class ServerInfo(projectName: String, endpoints: List[String]) \ No newline at end of file +final case class ServerInfo(projectName: String, endpoints: List[String]) diff --git a/actor-server/actor-models/src/main/protobuf/model/push.proto b/actor-server/actor-models/src/main/protobuf/model/push.proto index 519a51f64a..71667d4f8f 100644 --- a/actor-server/actor-models/src/main/protobuf/model/push.proto +++ b/actor-server/actor-models/src/main/protobuf/model/push.proto @@ -5,8 +5,16 @@ option java_package = "im.actor.server.model"; import "scalapb/scalapb.proto"; import "google/protobuf/wrappers.proto"; -message GooglePushCredentials { - option (scalapb.message).extends = "im.actor.server.model.push.PushCredentials"; +message GCMPushCredentials { + option (scalapb.message).extends = "im.actor.server.model.push.GooglePushCredentials"; + + int64 auth_id = 1; + int64 project_id = 2; + string reg_id = 3; +} + +message FirebasePushCredentials { + option (scalapb.message).extends = "im.actor.server.model.push.GooglePushCredentials"; int64 auth_id = 1; int64 project_id = 2; diff --git a/actor-server/actor-models/src/main/scala/im/actor/server/model/AuthSmsCodeObsolete.scala b/actor-server/actor-models/src/main/scala/im/actor/server/model/AuthSmsCodeObsolete.scala deleted file mode 100644 index 1c34d0c953..0000000000 --- a/actor-server/actor-models/src/main/scala/im/actor/server/model/AuthSmsCodeObsolete.scala +++ /dev/null @@ -1,4 +0,0 @@ -package im.actor.server.model - -@SerialVersionUID(1L) -case class AuthSmsCodeObsolete(id: Long, phoneNumber: Long, smsHash: String, smsCode: String, isDeleted: Boolean = false) \ No newline at end of file diff --git a/actor-server/actor-models/src/main/scala/im/actor/server/model/Department.scala b/actor-server/actor-models/src/main/scala/im/actor/server/model/Department.scala deleted file mode 100644 index c43f781d91..0000000000 --- a/actor-server/actor-models/src/main/scala/im/actor/server/model/Department.scala +++ /dev/null @@ -1,6 +0,0 @@ -package im.actor.server.model - -import com.github.tminglei.slickpg.LTree -import org.joda.time.DateTime - -case class Department(id: Int, name: String, struct: LTree, deletedAt: Option[DateTime] = None) \ No newline at end of file diff --git a/actor-server/actor-models/src/main/scala/im/actor/server/model/FileSourceBlock.scala b/actor-server/actor-models/src/main/scala/im/actor/server/model/FileSourceBlock.scala deleted file mode 100644 index 7cbba0892f..0000000000 --- a/actor-server/actor-models/src/main/scala/im/actor/server/model/FileSourceBlock.scala +++ /dev/null @@ -1,3 +0,0 @@ -package im.actor.server.model - -case class FileSourceBlock(fileId: Int, offset: Int, length: Int) diff --git a/actor-server/actor-models/src/main/scala/im/actor/server/model/Manager.scala b/actor-server/actor-models/src/main/scala/im/actor/server/model/Manager.scala deleted file mode 100644 index c9183a8210..0000000000 --- a/actor-server/actor-models/src/main/scala/im/actor/server/model/Manager.scala +++ /dev/null @@ -1,4 +0,0 @@ -package im.actor.server.model - -@SerialVersionUID(1L) -case class Manager(id: Int, name: String, lastName: String, domain: String, authToken: String, email: String) \ No newline at end of file diff --git a/actor-server/actor-models/src/main/scala/im/actor/server/model/MessageState.scala b/actor-server/actor-models/src/main/scala/im/actor/server/model/MessageState.scala deleted file mode 100644 index 5ff66a34ed..0000000000 --- a/actor-server/actor-models/src/main/scala/im/actor/server/model/MessageState.scala +++ /dev/null @@ -1,28 +0,0 @@ -package im.actor.server.model - -sealed trait MessageState { - def toInt: Int -} - -object MessageState { - @SerialVersionUID(1L) - case object Sent extends MessageState { - def toInt = 1 - } - - @SerialVersionUID(1L) - case object Received extends MessageState { - def toInt = 2 - } - - @SerialVersionUID(1L) - case object Read extends MessageState { - def toInt = 3 - } - - def fromInt(i: Int): MessageState = i match { - case 1 ⇒ Sent - case 2 ⇒ Received - case 3 ⇒ Read - } -} diff --git a/actor-server/actor-models/src/main/scala/im/actor/server/model/UserDepartment.scala b/actor-server/actor-models/src/main/scala/im/actor/server/model/UserDepartment.scala deleted file mode 100644 index 25c3e3b07b..0000000000 --- a/actor-server/actor-models/src/main/scala/im/actor/server/model/UserDepartment.scala +++ /dev/null @@ -1,3 +0,0 @@ -package im.actor.server.model - -case class UserDepartment(userId: Int, departmentId: Int) \ No newline at end of file diff --git a/actor-server/actor-models/src/main/scala/im/actor/server/model/UserPublicKey.scala b/actor-server/actor-models/src/main/scala/im/actor/server/model/UserPublicKey.scala deleted file mode 100644 index e19991194a..0000000000 --- a/actor-server/actor-models/src/main/scala/im/actor/server/model/UserPublicKey.scala +++ /dev/null @@ -1,8 +0,0 @@ -package im.actor.server.model - -@SerialVersionUID(1L) -case class UserPublicKey( - userId: Int, - hash: Long, - data: Array[Byte] -) diff --git a/actor-server/actor-models/src/main/scala/im/actor/server/model/push/PushCredentials.scala b/actor-server/actor-models/src/main/scala/im/actor/server/model/push/PushCredentials.scala index 6a15866d3b..2f9fc26212 100644 --- a/actor-server/actor-models/src/main/scala/im/actor/server/model/push/PushCredentials.scala +++ b/actor-server/actor-models/src/main/scala/im/actor/server/model/push/PushCredentials.scala @@ -4,3 +4,7 @@ trait PushCredentials { val authId: Long } +trait GooglePushCredentials extends PushCredentials { + val projectId: Long + val regId: String +} diff --git a/actor-server/actor-models/src/main/scala/im/actor/server/model/voximplant/VoxUser.scala b/actor-server/actor-models/src/main/scala/im/actor/server/model/voximplant/VoxUser.scala deleted file mode 100644 index 6a40703b1b..0000000000 --- a/actor-server/actor-models/src/main/scala/im/actor/server/model/voximplant/VoxUser.scala +++ /dev/null @@ -1,3 +0,0 @@ -package im.actor.server.model.voximplant - -case class VoxUser(userId: Int, voxUserId: Long, userName: String, displayName: String, salt: String) \ No newline at end of file diff --git a/actor-server/actor-persist/src/main/resources/sql/migration/V20160902182358__SchemaCleanup.sql b/actor-server/actor-persist/src/main/resources/sql/migration/V20160902182358__SchemaCleanup.sql new file mode 100644 index 0000000000..a6879db3a5 --- /dev/null +++ b/actor-server/actor-persist/src/main/resources/sql/migration/V20160902182358__SchemaCleanup.sql @@ -0,0 +1,13 @@ +drop table llectro_devices; +drop table llectro_interests; +drop table llectro_users; +drop table llectro_users_interests; +drop table managers; +drop table vox_users; +drop table user_department; +drop table webrtc_calls; +drop table auth_sms_codes_obsolete; +drop table file_datas; +drop table file_blocks; +drop table max_dates; +drop table plain_mails; diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/AuthSmsCodeObsoleteRepo.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/AuthSmsCodeObsoleteRepo.scala deleted file mode 100644 index 27275d06a4..0000000000 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/AuthSmsCodeObsoleteRepo.scala +++ /dev/null @@ -1,31 +0,0 @@ -package im.actor.server.persist - -import im.actor.server.model.AuthSmsCodeObsolete -import slick.driver.PostgresDriver.api._ - -final class AuthSmsCodeObsoleteTable(tag: Tag) extends Table[AuthSmsCodeObsolete](tag, "auth_sms_codes_obsolete") { - def id = column[Long]("id", O.PrimaryKey) - def phoneNumber = column[Long]("phone_number") - def smsHash = column[String]("sms_hash") - def smsCode = column[String]("sms_code") - def isDeleted = column[Boolean]("is_deleted") - - def * = (id, phoneNumber, smsHash, smsCode, isDeleted) <> (AuthSmsCodeObsolete.tupled, AuthSmsCodeObsolete.unapply) -} - -object AuthSmsCodeObsoleteRepo { - val codes = TableQuery[AuthSmsCodeObsoleteTable] - - def byPhoneNumber(number: Rep[Long]) = - codes.filter(c ⇒ c.phoneNumber === number && c.isDeleted === false) - private val byPhoneNumberC = Compiled(byPhoneNumber _) - - def create(id: Long, phoneNumber: Long, smsHash: String, smsCode: String) = - codes += AuthSmsCodeObsolete(id, phoneNumber, smsHash, smsCode) - - def findByPhoneNumber(number: Long) = - byPhoneNumberC(number).result - - def deleteByPhoneNumber(number: Long) = - byPhoneNumber(number).map(_.isDeleted).update(true) -} diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/DepartmentRepo.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/DepartmentRepo.scala deleted file mode 100644 index f483aa5dfb..0000000000 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/DepartmentRepo.scala +++ /dev/null @@ -1,43 +0,0 @@ -package im.actor.server.persist - -import com.github.tminglei.slickpg.LTree -import com.github.tototoshi.slick.PostgresJodaSupport._ -import org.joda.time.DateTime - -import im.actor.server._ -import im.actor.server.db.ActorPostgresDriver.api._ - -final class DepartmentTable(tag: Tag) extends Table[model.Department](tag, "departments") { - def id = column[Int]("id", O.PrimaryKey) - def name = column[String]("name") - def struct = column[LTree]("struct") - def deletedAt = column[Option[DateTime]]("deleted_at") - def structUnique = index("department_struct_idx", struct, unique = true) - - def * = (id, name, struct, deletedAt) <> (model.Department.tupled, model.Department.unapply) -} - -object DepartmentRepo { - - val departments = TableQuery[DepartmentTable] - - def create(department: model.Department) = - departments += department - - def find(struct: String) = - departments.filter(_.struct === LTree(struct)).result - - def setName(struct: String, name: String) = - departments.filter(_.struct === LTree(struct)).map(_.name).update(name) - - def setDeletedAt(struct: String) = - departments.filter(_.struct === LTree(struct)).map(_.deletedAt).update(Some(new DateTime)) - - def deptAndChildren(struct: String) = { - departments. - filter { e ⇒ LTree(struct).bind @> e.struct }. - sortBy { _.struct }. - result - } - -} \ No newline at end of file diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/Group.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/Group.scala index 25eec0a1ff..79e4decb5f 100644 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/Group.scala +++ b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/Group.scala @@ -93,11 +93,7 @@ object GroupRepo { ) } - //??? - def findPublic = - groups.filter(_.isPublic === true).map(_.asGroup).result - - @deprecated("Replace with some sort of key-value maybe?", "2016-06-05") + // TODO: Replace with key value def findAllIds = allIds.result @deprecated("Remove, only used in tests", "2016-06-05") @@ -122,9 +118,6 @@ object GroupRepo { def updateAbout(id: Int, about: Option[String]) = byIdC.applied(id).map(_.about).update(about) - //??? - def makePublic(id: Int) = byIdC.applied(id).map(_.isPublic).update(true) - @deprecated("Migrations only", "2016-06-05") def makeHidden(id: Int) = byIdC.applied(id).map(_.isHidden).update(true) } diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/GroupUserRepo.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/GroupUserRepo.scala index 8953c2cc1c..1a828f9827 100644 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/GroupUserRepo.scala +++ b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/GroupUserRepo.scala @@ -52,18 +52,14 @@ object GroupUserRepo { def find(groupId: Int) = byGroupIdC(groupId).result - @deprecated("Compatibility with old group API, remove when possible", "2016-06-05") - def find(groupId: Int, userId: Int) = - byPKC((groupId, userId)).result.headOption - - //TODO: remove - def exists(groupId: Int, userId: Int) = - byPKC.applied((groupId, userId)).exists.result - //TODO: revisit later def findUserIds(groupId: Int) = userIdByGroupIdC(groupId).result + @deprecated("Compatibility with old group API, remove when possible", "2016-06-05") + def find(groupId: Int, userId: Int) = + byPKC((groupId, userId)).result.headOption + @deprecated("Duplication of event-sourced groups logic", "2016-06-05") def delete(groupId: Int, userId: Int): FixedSqlAction[Int, NoStream, Write] = byPKC.applied((groupId, userId)).delete @@ -72,4 +68,8 @@ object GroupUserRepo { def makeAdmin(groupId: Int, userId: Int) = byPKC.applied((groupId, userId)).map(_.isAdmin).update(true) + @deprecated("Duplication of event-sourced groups logic", "2016-06-05") + def dismissAdmin(groupId: Int, userId: Int) = + byPKC.applied((groupId, userId)).map(_.isAdmin).update(false) + } diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/HistoryMessageRepo.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/HistoryMessageRepo.scala index d8aef50029..d7ed77caed 100644 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/HistoryMessageRepo.scala +++ b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/HistoryMessageRepo.scala @@ -2,7 +2,6 @@ package im.actor.server.persist import com.github.tototoshi.slick.PostgresJodaSupport._ import im.actor.server.model.{ Peer, PeerType, HistoryMessage } -import im.actor.server.persist.dialog.DialogRepo import org.joda.time.DateTime import slick.dbio.Effect.{ Write, Read } import slick.driver.PostgresDriver @@ -149,32 +148,13 @@ object HistoryMessageRepo { .map(_.userId) .result - def findNewest(userId: Int, peer: Peer): SqlAction[Option[HistoryMessage], NoStream, Read] = { - val filter = { m: HistoryMessageTable ⇒ - m.userId === userId && - m.peerType === peer.typ.value && - m.peerId === peer.id - } - findNewestFilter(userId, peer, filter) - } - - def findNewestSentBy(userId: Int, peer: Peer): SqlAction[Option[HistoryMessage], NoStream, Read] = { - val filter = { m: HistoryMessageTable ⇒ - m.senderUserId === userId && - m.peerType === peer.typ.value && - m.peerId === peer.id - } - findNewestFilter(userId, peer, filter) - } - - private def findNewestFilter(userId: Int, peer: Peer, filterClause: HistoryMessageTable ⇒ Rep[Boolean]) = { + def findNewest(userId: Int, peer: Peer): SqlAction[Option[HistoryMessage], NoStream, Read] = notDeletedMessages - .filter(filterClause) + .filter(m ⇒ m.userId === userId && m.peerType === peer.typ.value && m.peerId === peer.id) .sortBy(_.date.desc) .take(1) .result .headOption - } def find(userId: Int, peer: Peer): FixedSqlStreamingAction[Seq[HistoryMessage], HistoryMessage, Read] = notDeletedMessages @@ -233,7 +213,6 @@ object HistoryMessageRepo { .result def deleteAll(userId: Int, peer: Peer): FixedSqlAction[Int, NoStream, Write] = { - require(userId != SharedUserId, "Can't delete messages for shared user") notDeletedMessages .filter(m ⇒ m.userId === userId && m.peerType === peer.typ.value && m.peerId === peer.id) .map(_.deletedAt) @@ -246,4 +225,4 @@ object HistoryMessageRepo { .filter(_.randomId inSet randomIds) .map(_.deletedAt) .update(Some(new DateTime)) -} \ No newline at end of file +} diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/ManagerRepo.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/ManagerRepo.scala deleted file mode 100644 index 4cfdd37ef9..0000000000 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/ManagerRepo.scala +++ /dev/null @@ -1,27 +0,0 @@ -package im.actor.server.persist - -import im.actor.server.model.Manager -import slick.driver.PostgresDriver.api._ - -final class ManagerTable(tag: Tag) extends Table[Manager](tag, "managers") { - def id = column[Int]("id", O.PrimaryKey) - def name = column[String]("name") - def lastName = column[String]("last_name") - def domain = column[String]("domain") - def authToken = column[String]("auth_token") - def email = column[String]("email") - def emailUnique = index("manager_email_idx", email, unique = true) //way to keep email unique - - def * = (id, name, lastName, domain, authToken, email) <> (Manager.tupled, Manager.unapply) -} - -object ManagerRepo { - val managers = TableQuery[ManagerTable] - - def create(manager: Manager) = - managers += manager - - def findByEmail(email: String) = - managers.filter(_.email === email).result.headOption - -} \ No newline at end of file diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/MessageStateColumnType.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/MessageStateColumnType.scala deleted file mode 100644 index a236e291c7..0000000000 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/MessageStateColumnType.scala +++ /dev/null @@ -1,9 +0,0 @@ -package im.actor.server.persist - -import im.actor.server.model.MessageState -import slick.driver.PostgresDriver.api._ - -object MessageStateColumnType { - implicit val messageStateColumnType = - MappedColumnType.base[MessageState, Int](_.toInt, MessageState.fromInt) -} diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/UserDepartmentRepo.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/UserDepartmentRepo.scala deleted file mode 100644 index a98aee4d86..0000000000 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/UserDepartmentRepo.scala +++ /dev/null @@ -1,23 +0,0 @@ -package im.actor.server.persist - -import im.actor.server.db.ActorPostgresDriver.api._ -import im.actor.server.model.UserDepartment - -final class UserDepartmentTable(tag: Tag) extends Table[UserDepartment](tag, "user_department") { - def userId = column[Int]("user_id", O.PrimaryKey) - def departmentId = column[Int]("department_id", O.PrimaryKey) - - def * = (userId, departmentId) <> (UserDepartment.tupled, UserDepartment.unapply) -} - -object UserDepartmentRepo { - - val userDepartments = TableQuery[UserDepartmentTable] - - def create(userId: Int, departmentId: Int) = - userDepartments += UserDepartment(userId, departmentId) - - def userIdsByDepartmentId(deptId: Int) = - userDepartments.filter { _.departmentId === deptId }.map { _.userId }.result - -} \ No newline at end of file diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/UserPhoneRepo.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/UserPhoneRepo.scala index 2e3af39029..2dfe7a6d72 100644 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/UserPhoneRepo.scala +++ b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/UserPhoneRepo.scala @@ -18,11 +18,11 @@ final class UserPhoneTable(tag: Tag) extends Table[UserPhone](tag, "user_phones" object UserPhoneRepo { val phones = TableQuery[UserPhoneTable] - val byPhoneNumber = Compiled { number: Rep[Long] ⇒ + private val byPhoneNumber = Compiled { number: Rep[Long] ⇒ phones.filter(_.number === number) } - val phoneExists = Compiled { number: Rep[Long] ⇒ + private val phoneExists = Compiled { number: Rep[Long] ⇒ phones.filter(_.number === number).exists } @@ -37,15 +37,6 @@ object UserPhoneRepo { def findByUserId(userId: Int): FixedSqlStreamingAction[Seq[UserPhone], UserPhone, Read] = phones.filter(_.userId === userId).result - def findByUserIds(userIds: Set[Int]): FixedSqlStreamingAction[Seq[UserPhone], UserPhone, Read] = - phones.filter(_.userId inSet userIds).result - def create(id: Int, userId: Int, accessSalt: String, number: Long, title: String): FixedSqlAction[Int, NoStream, Write] = phones += UserPhone(id, userId, accessSalt, number, title) - - def create(userPhone: UserPhone): FixedSqlAction[Int, NoStream, Write] = - phones += userPhone - - def updateTitle(userId: Int, id: Int, title: String) = - phones.filter(p ⇒ p.userId === userId && p.id === id).map(_.title).update(title) } diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/UserPublicKeyRepo.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/UserPublicKeyRepo.scala deleted file mode 100644 index 19af43d104..0000000000 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/UserPublicKeyRepo.scala +++ /dev/null @@ -1,51 +0,0 @@ -package im.actor.server.persist - -import com.github.tototoshi.slick.PostgresJodaSupport._ -import im.actor.server.model.UserPublicKey -import org.joda.time.DateTime -import slick.driver.PostgresDriver.api._ - -final class UserPublicKeyTable(tag: Tag) extends Table[UserPublicKey](tag, "public_keys") { - def userId = column[Int]("user_id", O.PrimaryKey) - def hash = column[Long]("hash", O.PrimaryKey) - def data = column[Array[Byte]]("data") - def deletedAt = column[Option[DateTime]]("deleted_at") - - def * = (userId, hash, data) <> (UserPublicKey.tupled, UserPublicKey.unapply) -} - -object UserPublicKeyRepo { - val pkeys = TableQuery[UserPublicKeyTable] - - private def active = - pkeys.filter(_.deletedAt.isEmpty) - - private def activeByUserId(userId: Int) = - active.filter(p ⇒ p.userId === userId && p.deletedAt.isEmpty) - - def create(pk: UserPublicKey) = - pkeys += pk - - def delete(userId: Int, hash: Long) = - pkeys.filter(p ⇒ p.userId === userId && p.hash === hash).map(_.deletedAt).update(Some(new DateTime)) - - def find(userId: Int, hash: Long) = - active.filter(p ⇒ p.userId === userId && p.hash === hash).result - - def findByUserId(userId: Int) = - active.filter(_.userId === userId).result - - def findKeyHashes(userId: Int) = - activeByUserId(userId).map(_.hash).result - - def findByUserHashes(pairs: Set[(Int, Long)]) = { - // TODO: type-based size checking - require(pairs.size > 0) - - active.filter { pk ⇒ - pairs.view.map { - case (userId, hash) ⇒ pk.userId === userId && pk.hash === hash - }.reduceLeft(_ || _) - }.result - } -} diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/UserRepo.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/UserRepo.scala index bc649450e5..35391e0661 100644 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/UserRepo.scala +++ b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/UserRepo.scala @@ -40,11 +40,13 @@ object UserRepo { val byIdC = Compiled(byId _) val nameByIdC = Compiled(nameById _) - def byNickname(nickname: Rep[String]) = users filter (_.nickname.toLowerCase === nickname.toLowerCase) - def idsByNickname(nickname: Rep[String]) = byNickname(nickname).map(_.id) + private def byNickname(nickname: Rep[String]) = + users filter (_.nickname.toLowerCase === nickname.toLowerCase) + private def byNicknamePrefix(nickPrefix: Rep[String]) = + users filter (_.nickname.toLowerCase.like(nickPrefix.toLowerCase)) - val byNicknameC = Compiled(byNickname _) - val idsByNicknameC = Compiled(idsByNickname _) + private val byNicknameC = Compiled(byNickname _) + private val byNicknamePrefixC = Compiled(byNicknamePrefix _) def byPhone(phone: Rep[Long]) = (for { phones ← UserPhoneRepo.phones.filter(_.number === phone) @@ -106,14 +108,19 @@ object UserRepo { def findSalts(ids: Set[Int]) = users.filter(_.id inSet ids).map(u ⇒ (u.id, u.accessSalt)).result - def findByNickname(query: String) = { + @deprecated("user GlobalNamesStorageKeyValueStorage instead", "2016-07-17") + def findByNickname(query: String): DBIO[Option[User]] = { val nickname = if (query.startsWith("@")) query.drop(1) else query byNicknameC(nickname).result.headOption } - def findIdsByNickname(nickname: String) = - idsByNicknameC(nickname).result.headOption + @deprecated("user GlobalNamesStorageKeyValueStorage instead", "2016-07-17") + def findByNicknamePrefix(query: String): DBIO[Seq[User]] = { + val nickname: String = + if (query.startsWith("@")) query.drop(1) else query + byNicknamePrefixC(nickname).result + } def findIdsByEmail(email: String) = idsByEmailC(email).result.headOption @@ -121,19 +128,20 @@ object UserRepo { def findIds(query: String)(implicit ec: ExecutionContext) = for { e ← idsByEmailC(query).result - n ← idsByNicknameC(query).result p ← PhoneNumberUtils.normalizeStr(query) .headOption .map(idByPhoneC(_).result) .getOrElse(DBIO.successful(Nil)) - } yield e ++ n ++ p + } yield e ++ p + @deprecated("user GlobalNamesStorageKeyValueStorage instead", "2016-07-17") def setNickname(userId: Int, nickname: Option[String]) = byId(userId).map(_.nickname).update(nickname) def setAbout(userId: Int, about: Option[String]) = byId(userId).map(_.about).update(about) + @deprecated("user GlobalNamesStorageKeyValueStorage instead", "2016-07-17") def nicknameExists(nickname: String) = users.filter(_.nickname.toLowerCase === nickname.toLowerCase).exists.result diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/contact/UnregisteredEmailContactRepo.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/contact/UnregisteredEmailContactRepo.scala index 016b81783d..1853f58ab6 100644 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/contact/UnregisteredEmailContactRepo.scala +++ b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/contact/UnregisteredEmailContactRepo.scala @@ -12,14 +12,11 @@ final class UnregisteredEmailContactTable(tag: Tag) extends UnregisteredContactB } object UnregisteredEmailContactRepo { - val emailContacts = TableQuery[UnregisteredEmailContactTable] + private val emailContacts = TableQuery[UnregisteredEmailContactTable] - def create(email: String, ownerUserId: Int, name: Option[String]) = + private def create(email: String, ownerUserId: Int, name: Option[String]) = emailContacts += UnregisteredEmailContact(email, ownerUserId, name) - def create(contacts: Seq[UnregisteredEmailContact]) = - emailContacts ++= contacts - def createIfNotExists(email: String, ownerUserId: Int, name: Option[String]) = { create(email, ownerUserId, name).asTry } @@ -29,4 +26,4 @@ object UnregisteredEmailContactRepo { def deleteAll(email: String) = emailContacts.filter(_.email === email).delete -} \ No newline at end of file +} diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/contact/UnregisteredPhoneContactRepo.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/contact/UnregisteredPhoneContactRepo.scala index c1a20bfbb1..d4d52c3e05 100644 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/contact/UnregisteredPhoneContactRepo.scala +++ b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/contact/UnregisteredPhoneContactRepo.scala @@ -12,9 +12,9 @@ final class UnregisteredPhoneContactTable(tag: Tag) extends UnregisteredContactB } object UnregisteredPhoneContactRepo { - val phoneContacts = TableQuery[UnregisteredPhoneContactTable] + private val phoneContacts = TableQuery[UnregisteredPhoneContactTable] - def create(phoneNumber: Long, ownerUserId: Int, name: Option[String]) = + private def create(phoneNumber: Long, ownerUserId: Int, name: Option[String]) = phoneContacts += UnregisteredPhoneContact(phoneNumber, ownerUserId, name) def createIfNotExists(phoneNumber: Long, ownerUserId: Int, name: Option[String]) = { diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/dialog/DialogRepo.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/dialog/DialogRepo.scala index cb2a1d3e31..26622204a6 100644 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/dialog/DialogRepo.scala +++ b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/dialog/DialogRepo.scala @@ -4,13 +4,8 @@ import com.github.tototoshi.slick.PostgresJodaSupport._ import im.actor.server.db.ActorPostgresDriver.api._ import im.actor.server.model._ import org.joda.time.DateTime -import slick.dbio.DBIOAction -import slick.dbio.Effect.Read -import slick.lifted.ColumnOrdered -import slick.profile.FixedSqlStreamingAction import scala.concurrent.ExecutionContext -import scala.util.{ Failure, Success } final class DialogCommonTable(tag: Tag) extends Table[DialogCommon](tag, "dialog_commons") { @@ -111,49 +106,6 @@ final class UserDialogTable(tag: Tag) extends Table[UserDialog](tag, "user_dialo object DialogRepo extends UserDialogOperations with DialogCommonOperations { - def create(dialog: DialogObsolete)(implicit ec: ExecutionContext): DBIO[Int] = { - val dialogId = getDialogId(Some(dialog.userId), dialog.peer) - - val common = DialogCommon( - dialogId = dialogId, - lastMessageDate = dialog.lastMessageDate, - lastReceivedAt = dialog.lastReceivedAt, - lastReadAt = dialog.lastReadAt - ) - - val user = UserDialog( - userId = dialog.userId, - peer = dialog.peer, - ownerLastReceivedAt = dialog.ownerLastReceivedAt, - ownerLastReadAt = dialog.ownerLastReadAt, - createdAt = dialog.createdAt, - shownAt = dialog.shownAt, - isFavourite = dialog.isFavourite, - archivedAt = dialog.archivedAt - ) - - for { - exists ← commonExists(dialogId) - result ← if (exists) { - UserDialogRepo.userDialogs += user - } else { - for { - c ← (DialogCommonRepo.dialogCommon += common) - .asTry - .flatMap { - case Failure(e) ⇒ - commonExists(common.dialogId) flatMap { - case true ⇒ DBIO.successful(1) - case false ⇒ DBIO.failed(e) - } - case Success(res) ⇒ DBIO.successful(res) - } - _ ← UserDialogRepo.userDialogs += user - } yield c - } - } yield result - } - private val dialogs = for { c ← DialogCommonRepo.dialogCommon u ← UserDialogRepo.userDialogs if c.dialogId === repDialogId(u.userId, u.peerId, u.peerType) @@ -163,31 +115,17 @@ object DialogRepo extends UserDialogOperations with DialogCommonOperations { private val byUserC = Compiled(byUserId _) - private val archived = DialogRepo.dialogs.filter(_._2.archivedAt.isDefined) - - private val notArchived = DialogRepo.dialogs.filter(_._2.archivedAt.isEmpty) - - private def archivedByUserId( - userId: Rep[Int], - offset: ConstColumn[Long], - limit: ConstColumn[Long] - ) = archived filter (_._2.userId === userId) drop offset take limit - - private val archivedByUserIdC = Compiled(archivedByUserId _) - - private val archivedExistC = Compiled { (userId: Rep[Int]) ⇒ - archivedByUserId(userId, 0L, 1L).take(1).exists - } - private def byPKSimple(userId: Rep[Int], peerType: Rep[Int], peerId: Rep[Int]) = dialogs.filter({ case (_, u) ⇒ u.userId === userId && u.peerType === peerType && u.peerId === peerId }) private def byUserId(userId: Rep[Int]) = dialogs.filter({ case (_, u) ⇒ u.userId === userId }) + @deprecated("Migrations only", "2016-09-02") def findDialog(userId: Int, peer: Peer)(implicit ec: ExecutionContext): DBIO[Option[DialogObsolete]] = byPKC((userId, peer.typ.value, peer.id)).result.headOption map (_.map { case (c, u) ⇒ DialogObsolete.fromCommonAndUser(c, u) }) + @deprecated("Migrations only", "2016-09-02") def fetchDialogs(userId: Int)(implicit ec: ExecutionContext): DBIO[Seq[DialogObsolete]] = byUserC(userId).result map (_.map { case (c, u) ⇒ DialogObsolete.fromCommonAndUser(c, u) }) -} \ No newline at end of file +} diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/encryption/EphermalPublicKeyRepo.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/encryption/EphermalPublicKeyRepo.scala index a5b08a2c4a..0fbeae72b5 100644 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/encryption/EphermalPublicKeyRepo.scala +++ b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/encryption/EphermalPublicKeyRepo.scala @@ -44,4 +44,4 @@ object EphermalPublicKeyRepo { def fetch(userId: Int, keyGroupId: Int, keyIds: Set[Long]) = byUserIdKeyGroupC.applied(userId → keyGroupId).filter(_.keyId inSet keyIds).result -} \ No newline at end of file +} diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/push/FirebasePushCredentialsKV.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/push/FirebasePushCredentialsKV.scala new file mode 100644 index 0000000000..5fff148c6a --- /dev/null +++ b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/push/FirebasePushCredentialsKV.scala @@ -0,0 +1,65 @@ +package im.actor.server.persist.push + +import akka.actor.ActorSystem +import akka.http.scaladsl.util.FastFuture +import com.google.protobuf.wrappers.Int64Value +import im.actor.server.db.DbExtension +import im.actor.server.model.push.FirebasePushCredentials +import im.actor.storage.SimpleStorage + +import scala.concurrent.Future + +// authId -> FirebasePushCredentials +private object FirebaseAuthIdCreds extends SimpleStorage("firebase_auth_id_creds") + +// token -> authId +private object FirebaseTokenAuthId extends SimpleStorage("firebase_token_auth_id") + +final class FirebasePushCredentialsKV(implicit system: ActorSystem) { + import system.dispatcher + + private val (db, conn) = { + val ext = DbExtension(system) + (ext.db, ext.connector) + } + + def createOrUpdate(creds: FirebasePushCredentials): Future[Unit] = + for { + _ ← conn.run( + FirebaseAuthIdCreds.upsert(creds.authId.toString, creds.toByteArray) + ) + _ ← conn.run( + FirebaseTokenAuthId.upsert(creds.regId, Int64Value(creds.authId).toByteArray) + ) + } yield () + + def deleteByToken(token: String): Future[Unit] = + for { + authIdOpt ← findAuthIdByToken(token) + _ ← authIdOpt map { authId ⇒ + delete(token, authId) + } getOrElse FastFuture.successful(None) + } yield () + + def findByToken(token: String): Future[Option[FirebasePushCredentials]] = + for { + authIdOpt ← findAuthIdByToken(token) + creds ← (authIdOpt map find) getOrElse FastFuture.successful(None) + } yield creds + + def find(authId: Long): Future[Option[FirebasePushCredentials]] = + conn.run(FirebaseAuthIdCreds.get(authId.toString)) map (_.map(FirebasePushCredentials.parseFrom)) + + def find(authIds: Set[Long]): Future[Seq[FirebasePushCredentials]] = + Future.sequence(authIds map find) map (_.flatten.toSeq) + + private def findAuthIdByToken(token: String): Future[Option[Long]] = + conn.run(FirebaseTokenAuthId.get(token)) map (_.map(e ⇒ Int64Value.parseFrom(e).value)) + + private def delete(token: String, authId: Long): Future[Unit] = + for { + _ ← conn.run(FirebaseAuthIdCreds.delete(authId.toString)) + _ ← conn.run(FirebaseTokenAuthId.delete(token)) + } yield () + +} diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/push/GooglePushCredentialsRepo.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/push/GooglePushCredentialsRepo.scala index 9c631daff6..c3e892d314 100644 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/push/GooglePushCredentialsRepo.scala +++ b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/push/GooglePushCredentialsRepo.scala @@ -1,26 +1,26 @@ package im.actor.server.persist.push -import im.actor.server.model.push.GooglePushCredentials +import im.actor.server.model.push.GCMPushCredentials import im.actor.server.persist.AuthIdRepo import slick.driver.PostgresDriver.api._ import scala.concurrent.ExecutionContext import scala.language.postfixOps -final class GooglePushCredentialsTable(tag: Tag) extends Table[GooglePushCredentials](tag, "google_push_credentials") { +final class GooglePushCredentialsTable(tag: Tag) extends Table[GCMPushCredentials](tag, "google_push_credentials") { def authId = column[Long]("auth_id", O.PrimaryKey) def projectId = column[Long]("project_id") def regId = column[String]("reg_id") - def * = (authId, projectId, regId) <> ((GooglePushCredentials.apply _).tupled, GooglePushCredentials.unapply) + def * = (authId, projectId, regId) <> ((GCMPushCredentials.apply _).tupled, GCMPushCredentials.unapply) } object GooglePushCredentialsRepo { private val creds = TableQuery[GooglePushCredentialsTable] - def createOrUpdate(c: GooglePushCredentials) = + def createOrUpdate(c: GCMPushCredentials) = creds.insertOrUpdate(c) private def byAuthId(authId: Rep[Long]) = creds.filter(_.authId === authId) @@ -39,7 +39,7 @@ object GooglePushCredentialsRepo { creds ← find(authIds map (_.id) toSet) } yield creds - def findByToken(token: String): DBIO[Option[GooglePushCredentials]] = + def findByToken(token: String): DBIO[Option[GCMPushCredentials]] = creds.filter(_.regId === token).result.headOption def delete(authId: Long) = diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/voximplant/VoxUser.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/voximplant/VoxUser.scala deleted file mode 100644 index 2b0e79a9f0..0000000000 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/voximplant/VoxUser.scala +++ /dev/null @@ -1,27 +0,0 @@ -package im.actor.server.persist.voximplant - -import im.actor.server.model.voximplant.{ VoxUser ⇒ VoxUserModel } -import slick.driver.PostgresDriver.api._ - -class VoxUserTable(tag: Tag) extends Table[VoxUserModel](tag, "vox_users") { - def userId = column[Int]("user_id", O.PrimaryKey) - def voxUserId = column[Long]("vox_user_id") - def userName = column[String]("user_name") - def displayName = column[String]("display_name") - def salt = column[String]("salt") - - def * = (userId, voxUserId, userName, displayName, salt) <> (VoxUserModel.tupled, VoxUserModel.unapply) -} - -object VoxUser { - val users = TableQuery[VoxUserTable] - - def create(user: VoxUserModel) = - users += user - - def createOrReplace(user: VoxUserModel) = - users.insertOrUpdate(user) - - def findByUserId(userId: Int) = - users.filter(_.userId === userId).result.headOption -} diff --git a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/webrtc/WebrtcCallRepo.scala b/actor-server/actor-persist/src/main/scala/im/actor/server/persist/webrtc/WebrtcCallRepo.scala deleted file mode 100644 index fd8cea2603..0000000000 --- a/actor-server/actor-persist/src/main/scala/im/actor/server/persist/webrtc/WebrtcCallRepo.scala +++ /dev/null @@ -1,28 +0,0 @@ -package im.actor.server.persist.webrtc - -import im.actor.server.db.ActorPostgresDriver.api._ -import im.actor.server.webrtc.WebrtcCall - -final class WebrtcCallTable(tag: Tag) extends Table[WebrtcCall](tag, "webrtc_calls") { - def id = column[Long]("id", O.PrimaryKey) - - def initiatorUserId = column[Int]("initiator_user_id") - - def receiverUserId = column[Int]("receiver_user_id") - - def * = (id, initiatorUserId, receiverUserId) <> ((WebrtcCall.apply _).tupled, WebrtcCall.unapply) -} - -object WebrtcCallRepo { - val webrtcCalls = TableQuery[WebrtcCallTable] - - val byPKC = Compiled { id: Rep[Long] ⇒ - webrtcCalls filter (_.id === id) - } - - def create(call: WebrtcCall) = webrtcCalls += call - - def find(id: Long) = byPKC(id).result.headOption - - def delete(id: Long) = byPKC(id).delete -} \ No newline at end of file diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/api/rpc/EntitiesHelpers.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/api/rpc/EntitiesHelpers.scala new file mode 100644 index 0000000000..9fba44017c --- /dev/null +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/api/rpc/EntitiesHelpers.scala @@ -0,0 +1,149 @@ +package im.actor.api.rpc + +import akka.actor.ActorSystem +import im.actor.api.rpc.groups.ApiGroup +import im.actor.api.rpc.messaging._ +import im.actor.api.rpc.peers.{ ApiGroupOutPeer, ApiPeerType, ApiUserOutPeer } +import im.actor.api.rpc.users.ApiUser +import im.actor.server.dialog.HistoryUtils +import im.actor.server.group.GroupExtension +import im.actor.server.user.UserExtension + +import scala.concurrent.Future + +object EntitiesHelpers { + + private type UsersOrPeers = (Vector[ApiUser], Vector[ApiUserOutPeer]) + + private type GroupsOrPeers = (Vector[ApiGroup], Vector[ApiGroupOutPeer]) + + /** + * Load users and groups presented in `dialogs`. + * If `stripEntities = true`, return user and group peers instead of groups and users + * If `loadGroupMembers = true` members will be presented in `ApiGroup` object and loaded as users/user peers, + * otherwise exclude members from users/user peers and `ApiGroup` object. + */ + def usersAndGroupsByDialogs( + dialogs: Seq[ApiDialog], + stripEntities: Boolean, + loadGroupMembers: Boolean + )(implicit client: AuthorizedClientData, system: ActorSystem): Future[(UsersOrPeers, GroupsOrPeers)] = { + val (userIds, groupIds) = dialogs.foldLeft((Set.empty[Int], Set.empty[Int])) { + case ((uacc, gacc), dialog) ⇒ + dialog.peer.`type` match { + case ApiPeerType.Private | ApiPeerType.EncryptedPrivate ⇒ + (uacc ++ relatedUsers(dialog.message) ++ Set(dialog.peer.id, dialog.senderUserId), gacc) + case ApiPeerType.Group ⇒ + (uacc ++ relatedUsers(dialog.message) + dialog.senderUserId, gacc + dialog.peer.id) + } + } + usersAndGroupsByIds(groupIds, userIds, stripEntities, loadGroupMembers) + } + + def usersAndGroupsByShortDialogs( + dialogs: Seq[ApiDialogShort], + stripEntities: Boolean, + loadGroupMembers: Boolean + )(implicit client: AuthorizedClientData, system: ActorSystem): Future[(UsersOrPeers, GroupsOrPeers)] = { + val (userIds, groupIds) = dialogs.foldLeft((Set.empty[Int], Set.empty[Int])) { + case ((uids, gids), dialog) ⇒ + dialog.peer.`type` match { + case ApiPeerType.Group ⇒ (uids, gids + dialog.peer.id) + case ApiPeerType.Private | ApiPeerType.EncryptedPrivate ⇒ (uids + dialog.peer.id, gids) + } + } + usersAndGroupsByIds(groupIds, userIds, stripEntities, loadGroupMembers) + } + + def usersAndGroupsByIds( + groupIds: Set[Int], + userIds: Set[Int], + stripEntities: Boolean, + loadGroupMembers: Boolean + )(implicit client: AuthorizedClientData, system: ActorSystem): Future[(UsersOrPeers, GroupsOrPeers)] = { + import system.dispatcher + + for { + (groupsOrPeers, groupUserIds) ← groupsOrPeers(groupIds, stripEntities, loadGroupMembers) + usersOrPeers ← usersOrPeers((userIds ++ groupUserIds).toVector, stripEntities) + } yield ( + usersOrPeers, + groupsOrPeers + ) + } + + // get groups or group peers and ids of group members if needed + private def groupsOrPeers( + groupIds: Set[Int], + stripEntities: Boolean, + loadGroupMembers: Boolean + )(implicit client: AuthorizedClientData, system: ActorSystem): Future[(GroupsOrPeers, Set[Int])] = { + import system.dispatcher + + for { + groups ← Future.sequence(groupIds map (GroupExtension(system).getApiStruct(_, client.userId, loadGroupMembers))) + groupUserIds = if (loadGroupMembers) + groups.flatMap(g ⇒ g.members.flatMap(m ⇒ Seq(m.userId, m.inviterUserId)) :+ g.creatorUserId) + else + Set.empty[Int] + groupsOrPeers = if (stripEntities) { + Vector.empty[ApiGroup] → (groups map (g ⇒ ApiGroupOutPeer(g.id, g.accessHash))).toVector + } else { + groups.toVector → Vector.empty[ApiGroupOutPeer] + } + } yield ( + groupsOrPeers, + groupUserIds + ) + } + + // TODO: merge together with method in GroupServiceImpl + def usersOrPeers(userIds: Vector[Int], stripEntities: Boolean)(implicit client: AuthorizedClientData, system: ActorSystem): Future[UsersOrPeers] = { + import system.dispatcher + if (stripEntities) { + val users = Vector.empty[ApiUser] + val peers = Future.sequence(userIds filterNot (_ == HistoryUtils.SharedUserId) map { userId ⇒ + UserExtension(system).getAccessHash(userId, client.authId) map (hash ⇒ ApiUserOutPeer(userId, hash)) + }) + peers map (users → _) + } else { + val users = Future.sequence(userIds filterNot (_ == HistoryUtils.SharedUserId) map { userId ⇒ + UserExtension(system).getApiStruct(userId, client.userId, client.authId) + }) + val peers = Vector.empty[ApiUserOutPeer] + users map (_ → peers) + } + } + + def relatedUsers(message: ApiMessage): Set[Int] = { + message match { + case ApiServiceMessage(_, extOpt) ⇒ extOpt map relatedUsers getOrElse Set.empty + case ApiTextMessage(_, mentions, _) ⇒ mentions.toSet + case _: ApiJsonMessage ⇒ Set.empty + case _: ApiEmptyMessage ⇒ Set.empty + case _: ApiDocumentMessage ⇒ Set.empty + case _: ApiStickerMessage ⇒ Set.empty + case _: ApiUnsupportedMessage ⇒ Set.empty + case _: ApiBinaryMessage ⇒ Set.empty + case _: ApiEncryptedMessage ⇒ Set.empty + } + } + + private def relatedUsers(ext: ApiServiceEx): Set[Int] = + ext match { + case ApiServiceExContactRegistered(userId) ⇒ Set(userId) + case ApiServiceExChangedAvatar(_) ⇒ Set.empty + case ApiServiceExChangedTitle(_) ⇒ Set.empty + case ApiServiceExChangedTopic(_) ⇒ Set.empty + case ApiServiceExChangedAbout(_) ⇒ Set.empty + case ApiServiceExGroupCreated | _: ApiServiceExGroupCreated ⇒ Set.empty + case ApiServiceExPhoneCall(_) ⇒ Set.empty + case ApiServiceExPhoneMissed | _: ApiServiceExPhoneMissed ⇒ Set.empty + case ApiServiceExUserInvited(invitedUserId) ⇒ Set(invitedUserId) + case ApiServiceExUserJoined | _: ApiServiceExUserJoined ⇒ Set.empty + case ApiServiceExUserKicked(kickedUserId) ⇒ Set(kickedUserId) + case ApiServiceExUserLeft | _: ApiServiceExUserLeft ⇒ Set.empty + case _: ApiServiceExChatArchived | _: ApiServiceExChatRestored ⇒ Set.empty + } + +} diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/api/rpc/PeerHelpers.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/api/rpc/PeerHelpers.scala index 6d67738c4e..52e6b8d0e1 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/api/rpc/PeerHelpers.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/api/rpc/PeerHelpers.scala @@ -7,7 +7,7 @@ import im.actor.api.rpc.CommonRpcErrors.InvalidAccessHash import im.actor.api.rpc.peers._ import im.actor.server.acl.ACLUtils._ import im.actor.server.db.DbExtension -import im.actor.server.group.GroupErrors.GroupNotFound +import im.actor.server.group.GroupErrors.{ GroupAlreadyDeleted, GroupNotFound } import im.actor.server.user.UserErrors.UserNotFound import slick.dbio.DBIO @@ -62,15 +62,6 @@ object PeerHelpers { accessHashCheck(checkGroupOutPeers(groupOutPeers), authorizedAction) } - //TODO: remove in future - @deprecated("Use Future inner type instead", "2016-07-07") - def withOutPeerDBIO[R <: RpcResponse](outPeer: ApiOutPeer)(f: ⇒ DBIO[RpcError Xor R])( - implicit - client: AuthorizedClientData, - system: ActorSystem - ): DBIO[RpcError Xor R] = - DBIO.from(withOutPeer(outPeer)(DbExtension(system).db.run(f))) - private def accessHashCheck[R <: RpcResponse](check: Future[Boolean], authorizedAction: ⇒ Future[RpcError Xor R])(implicit ec: ExecutionContext) = check flatMap { isValid ⇒ if (isValid) { @@ -83,8 +74,9 @@ object PeerHelpers { } private def handleNotFound: PartialFunction[Throwable, RpcError] = { - case _: UserNotFound ⇒ CommonRpcErrors.UserNotFound - case _: GroupNotFound ⇒ CommonRpcErrors.GroupNotFound - case e ⇒ throw e + case _: UserNotFound ⇒ CommonRpcErrors.UserNotFound + case _: GroupNotFound ⇒ CommonRpcErrors.GroupNotFound + case _: GroupAlreadyDeleted ⇒ CommonRpcErrors.GroupDeleted + case e ⇒ throw e } } diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/auth/AuthHelpers.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/auth/AuthHelpers.scala index 01ba65d1b5..4a8471313c 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/auth/AuthHelpers.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/auth/AuthHelpers.scala @@ -16,7 +16,6 @@ import im.actor.server.model._ import im.actor.server.persist._ import im.actor.server.persist.auth.AuthTransactionRepo import im.actor.server.session._ -import im.actor.util.misc.EmailUtils.isTestEmail import im.actor.util.misc.IdUtils._ import im.actor.util.misc.PhoneNumberUtils._ import im.actor.util.misc.StringUtils.validName @@ -25,7 +24,6 @@ import org.joda.time.DateTime import slick.dbio._ import scala.concurrent.Future -import scala.util.Try trait AuthHelpers extends Helpers { self: AuthServiceImpl ⇒ @@ -69,10 +67,10 @@ trait AuthHelpers extends Helpers { protected def newUsernameSignUp(transaction: AuthUsernameTransaction, name: String, sex: Option[ApiSex]): Result[(Int, String) Xor User] = { val username = transaction.username for { - optUser ← fromDBIO(UserRepo.findByNickname(username)) - result ← optUser match { - case Some(existingUser) ⇒ point(Xor.left((existingUser.id, ""))) - case None ⇒ newUser(name, "", sex, username = Some(username)) + optUserId ← fromFuture(globalNamesStorage.getUserId(username)) + result ← optUserId match { + case Some(id) ⇒ point(Xor.left((id, ""))) + case None ⇒ newUser(name, "", sex, username = Some(username)) } } yield result } @@ -170,8 +168,8 @@ trait AuthHelpers extends Helpers { } yield (emailModel.userId, "") case u: AuthUsernameTransaction ⇒ for { - userModel ← fromDBIOOption(AuthErrors.UsernameUnoccupied)(UserRepo.findByNickname(u.username)) - } yield (userModel.id, "") + userId ← fromFutureOption(AuthErrors.UsernameUnoccupied)(globalNamesStorage.getUserId(u.username)) + } yield (userId, "") case _: AuthAnonymousTransaction ⇒ fromEither(Xor.left(AuthErrors.NotValidated)) } diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/auth/AuthServiceImpl.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/auth/AuthServiceImpl.scala index 6c44c5b861..bfa32c52a8 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/auth/AuthServiceImpl.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/auth/AuthServiceImpl.scala @@ -21,9 +21,10 @@ import im.actor.server.auth.DeviceInfo import im.actor.server.db.DbExtension import im.actor.server.email.{ EmailConfig, SmtpEmailSender } import im.actor.server.model._ +import im.actor.server.names.GlobalNamesStorageKeyValueStorage import im.actor.server.oauth.GoogleProvider import im.actor.server.persist._ -import im.actor.server.persist.auth.{ AuthUsernameTransactionRepo, AuthPhoneTransactionRepo, AuthTransactionRepo, AuthEmailTransactionRepo } +import im.actor.server.persist.auth.{ AuthEmailTransactionRepo, AuthPhoneTransactionRepo, AuthTransactionRepo, AuthUsernameTransactionRepo } import im.actor.server.session._ import im.actor.server.social.{ SocialExtension, SocialManagerRegion } import im.actor.server.user.{ UserErrors, UserExtension } @@ -59,6 +60,7 @@ final class AuthServiceImpl(val oauth2Service: GoogleProvider)( protected val userExt = UserExtension(actorSystem) protected implicit val socialRegion: SocialManagerRegion = SocialExtension(actorSystem).region protected val activationContext = new ActivationContext + protected val globalNamesStorage = new GlobalNamesStorageKeyValueStorage private implicit val mat = ActorMaterializer() @@ -200,8 +202,8 @@ final class AuthServiceImpl(val oauth2Service: GoogleProvider)( val action = for { normUsername ← fromOption(ProfileRpcErrors.NicknameInvalid)(StringUtils.normalizeUsername(username)) - optUser ← fromDBIO(UserRepo.findByNickname(username)) - _ ← optUser map (u ⇒ forbidDeletedUser(u.id)) getOrElse point(()) + optUserId ← fromFuture(globalNamesStorage.getUserId(username)) + _ ← optUserId map (id ⇒ forbidDeletedUser(id)) getOrElse point(()) optAuthTransaction ← fromDBIO(AuthUsernameTransactionRepo.find(username, deviceHash)) transactionHash ← optAuthTransaction match { case Some(transaction) ⇒ point(transaction.transactionHash) @@ -210,7 +212,7 @@ final class AuthServiceImpl(val oauth2Service: GoogleProvider)( val transactionHash = ACLUtils.authTransactionHash(accessSalt) val authTransaction = AuthUsernameTransaction( normUsername, - optUser map (_.id), + optUserId, transactionHash, appId, apiKey, @@ -218,11 +220,11 @@ final class AuthServiceImpl(val oauth2Service: GoogleProvider)( deviceTitle, accessSalt, DeviceInfo(timeZone.getOrElse(""), preferredLanguages).toByteArray, - isChecked = optUser.isEmpty // we don't need to check password if user signs up + isChecked = optUserId.isEmpty // we don't need to check password if user signs up ) for (_ ← fromDBIO(AuthUsernameTransactionRepo.create(authTransaction))) yield transactionHash } - } yield ResponseStartUsernameAuth(transactionHash, optUser.isDefined) + } yield ResponseStartUsernameAuth(transactionHash, optUserId.isDefined) db.run(action.value) } @@ -241,7 +243,7 @@ final class AuthServiceImpl(val oauth2Service: GoogleProvider)( for { normUsername ← fromOption(ProfileRpcErrors.NicknameInvalid)(StringUtils.normalizeUsername(username)) accessSalt = ACLUtils.nextAccessSalt() - nicknameExists ← fromDBIO(UserRepo.nicknameExists(normUsername)) + nicknameExists ← fromFuture(globalNamesStorage.exists(normUsername)) _ ← fromBoolean(ProfileRpcErrors.NicknameBusy)(!nicknameExists) transactionHash = ACLUtils.authTransactionHash(accessSalt) transaction = AuthAnonymousTransaction( diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/auth/Helpers.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/auth/Helpers.scala index 94ccace486..90a110de14 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/auth/Helpers.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/auth/Helpers.scala @@ -1,5 +1,6 @@ package im.actor.server.api.rpc.service.auth +import cats.MonadCombine import cats.data.{ NonEmptyList, Xor } import cats.syntax.all._ import im.actor.api.rpc._ @@ -9,18 +10,19 @@ import org.apache.commons.validator.routines.EmailValidator private[auth] trait Helpers extends PublicKeyHelpers { private def matchesEmail(s: String): NonEmptyList[String] Xor String = - if (EmailValidator.getInstance().isValid(s)) s.right else NonEmptyList("Should be valid email address").left + if (EmailValidator.getInstance().isValid(s)) s.right else NonEmptyList.of("Should be valid email address").left def validEmail(email: String): NonEmptyList[String] Xor String = StringUtils.nonEmptyString(email).flatMap(e ⇒ matchesEmail(e.toLowerCase)) - private implicit val listMonadCombine = new cats.MonadCombine[List] { + private implicit val listMonadCombine = new MonadCombine[List] { def pure[A](x: A): List[A] = List(x) - def combine[A](x: List[A], y: List[A]): List[A] = x ::: y def flatMap[A, B](fa: List[A])(f: (A) ⇒ List[B]): List[B] = fa flatMap f def empty[A]: List[A] = List.empty[A] + def combineK[A](x: List[A], y: List[A]): List[A] = x ::: y + def tailRecM[A, B](a: A)(f: (A) ⇒ List[Either[A, B]]): List[B] = defaultTailRecM(a)(f) } def validationFailed(errorName: String, errors: NonEmptyList[String]): RpcError = - RpcError(400, errorName, errors.unwrap.mkString(", "), false, None) + RpcError(400, errorName, errors.toList.mkString(", "), false, None) } diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/contacts/ContactsServiceImpl.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/contacts/ContactsServiceImpl.scala index 58a6d43240..5ac43701dc 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/contacts/ContactsServiceImpl.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/contacts/ContactsServiceImpl.scala @@ -24,6 +24,7 @@ import im.actor.api.rpc.misc._ import im.actor.api.rpc.sequence.ApiUpdateOptimization import im.actor.api.rpc.users.ApiUser import im.actor.server.db.DbExtension +import im.actor.server.names.GlobalNamesStorageKeyValueStorage import im.actor.server.sequence.{ SeqState, SeqUpdatesExtension } import im.actor.server.social.{ SocialExtension, SocialManager, SocialManagerRegion } import im.actor.server.user._ @@ -49,6 +50,7 @@ class ContactsServiceImpl(implicit actorSystem: ActorSystem) private val userExt = UserExtension(actorSystem) private implicit val seqUpdExt: SeqUpdatesExtension = SeqUpdatesExtension(actorSystem) private implicit val socialRegion: SocialManagerRegion = SocialExtension(actorSystem).region + private val globalNamesStorage = new GlobalNamesStorageKeyValueStorage case class EmailNameUser(email: String, name: Option[String], userId: Int) @@ -170,8 +172,8 @@ class ContactsServiceImpl(implicit actorSystem: ActorSystem) private def findByNickname(nickname: String, client: AuthorizedClientData): Result[Vector[ApiUser]] = { for { - users ← fromDBIO(UserRepo.findByNickname(nickname) map (_.toList)) - structs ← fromFuture(Future.sequence(users map (user ⇒ userExt.getApiStruct(user.id, client.userId, client.authId)))) + optUserId ← fromFuture(globalNamesStorage.getUserId(nickname)) + structs ← fromFuture(Future.sequence(optUserId.toSeq map (userId ⇒ userExt.getApiStruct(userId, client.userId, client.authId)))) } yield structs.toVector } diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/encryption/EncryptionServiceImpl.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/encryption/EncryptionServiceImpl.scala index 37be9d987e..24fe5a5732 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/encryption/EncryptionServiceImpl.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/encryption/EncryptionServiceImpl.scala @@ -121,8 +121,6 @@ final class EncryptionServiceImpl(implicit system: ActorSystem) extends Encrypti encExt.checkBox(encryptedBox, ignoredKeyGroups.groupBy(_.userId).mapValues(_.map(_.keyGroupId).toSet)) flatMap { case Left((missing, obs)) ⇒ FastFuture.successful(Ok(ResponseSendEncryptedPackage( - seq = None, - state = None, date = None, obsoleteKeyGroups = obs, missedKeyGroups = missing @@ -159,8 +157,6 @@ final class EncryptionServiceImpl(implicit system: ActorSystem) extends Encrypti case None ⇒ updExt.deliverClientUpdate(client.userId, client.authId, UpdateEmptyUpdate) } } yield Ok(ResponseSendEncryptedPackage( - seq = Some(seqState.seq), - state = Some(seqState.state.toByteArray), date = Some(date), Vector.empty, Vector.empty diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/files/FilesServiceImpl.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/files/FilesServiceImpl.scala index ec63b7cd32..f75bfaf914 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/files/FilesServiceImpl.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/files/FilesServiceImpl.scala @@ -6,7 +6,7 @@ import java.time.temporal.ChronoUnit import akka.actor._ import akka.http.scaladsl.util.FastFuture import cats.data.Xor -import im.actor.api.rpc.CommonRpcErrors.IntenalError +import im.actor.api.rpc.CommonRpcErrors.InternalError import im.actor.api.rpc.FileRpcErrors.UnsupportedSignatureAlgorithm import im.actor.api.rpc._ import im.actor.api.rpc.files._ diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/groups/GroupRpcErrors.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/groups/GroupRpcErrors.scala index d40f68de22..e3332b1cfc 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/groups/GroupRpcErrors.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/groups/GroupRpcErrors.scala @@ -1,19 +1,31 @@ package im.actor.server.api.rpc.service.groups -import im.actor.api.rpc.RpcError +import im.actor.api.rpc.{ CommonRpcErrors, RpcError } // format: OFF object GroupRpcErrors { - val AlreadyInvited = RpcError(400, "USER_ALREADY_INVITED", "You are already invited to this group.", false, None) - val AlreadyJoined = RpcError(400, "USER_ALREADY_JOINED", "You are already a member of this group.", false, None) - val NotAMember = RpcError(403, "FORBIDDEN", "You are not a group member.", false, None) - val InvalidTitle = RpcError(400, "GROUP_TITLE_INVALID", "Invalid group title.", false, None) - val TopicTooLong = RpcError(400, "GROUP_TOPIC_TOO_LONG", "Group topic is too long. It should be no longer then 255 characters", false, None) - val AboutTooLong = RpcError(400, "GROUP_ABOUT_TOO_LONG", "Group about is too long. It should be no longer then 255 characters", false, None) - val UserAlreadyAdmin = RpcError(400, "USER_ALREADY_ADMIN", "User is already admin of this group", false, None) - val BlockedByUser = RpcError(403, "BLOCKED_BY_USER", "User blocked you, unable to invite him.", false, None) - val GroupIdAlreadyExists = RpcError(400, "GROUP_ALREADY_EXISTS", "Group with such id already exists", false, None) - val InvalidInviteToken = RpcError(403, "INVALID_INVITE_TOKEN", "No correct token provided.", false, None) - val GroupNotPublic = RpcError(400, "GROUP_IS_NOT_PUBLIC", "The group is not public.", false, None) + val AlreadyInvited = RpcError(400, "USER_ALREADY_INVITED", "You are already invited to this group.", false, None) + val AlreadyJoined = RpcError(400, "USER_ALREADY_JOINED", "You are already a member of this group.", false, None) + val NotAMember = RpcError(403, "FORBIDDEN", "You are not a group member.", false, None) + val InvalidTitle = RpcError(400, "GROUP_TITLE_INVALID", "Invalid group title.", false, None) + val TopicTooLong = RpcError(400, "GROUP_TOPIC_TOO_LONG", "Group topic is too long. It should be no longer then 255 characters", false, None) + val AboutTooLong = RpcError(400, "GROUP_ABOUT_TOO_LONG", "Group about is too long. It should be no longer then 255 characters", false, None) + val UserAlreadyAdmin = RpcError(400, "USER_ALREADY_ADMIN", "User is already admin of this group", false, None) + val UserAlreadyNotAdmin = RpcError(400, "USER_ALREADY_NOT_ADMIN", "User is already notadmin of this group", false, None) + val BlockedByUser = RpcError(403, "BLOCKED_BY_USER", "User blocked you, unable to invite him.", false, None) + val GroupIdAlreadyExists = RpcError(400, "GROUP_ALREADY_EXISTS", "Group with such id already exists.", false, None) + val InvalidInviteUrl = RpcError(403, "INVALID_INVITE_URL", "Invalid invite url!", false, None) + val InvalidInviteToken = RpcError(403, "INVALID_INVITE_TOKEN", "Invalid invite token!", false, None) + val InvalidInviteGroup = RpcError(403, "INVALID_INVITE_GROUP", "Invalid group name provided!", false, None) + val GroupNotPublic = RpcError(400, "GROUP_IS_NOT_PUBLIC", "The group is not public.", false, None) + val CantLeaveGroup = RpcError(403, "CANT_LEAVE_GROUP", "You can't leave this group!", false, None) + val CantJoinGroup = RpcError(403, "CANT_JOIN_GROUP", "You can't join this group!", false, None) + val CantGrantToBot = RpcError(400, "CANT_GRANT_TO_BOT", "Can't grant this permissions to bot", false, None) + val UserIsBanned = RpcError(403, "USER_IS_BANNED", "You can't join this group.", false, None) + + val InvalidShortName = RpcError(400, "GROUP_SHORT_NAME_INVALID", + "Invalid group short name. Valid short name should contain from 5 to 32 characters, and may consist of latin characters, numbers and underscores", false, None) + val ShortNameTaken = RpcError(400, "GROUP_SHORT_NAME_TAKEN", "This short name already belongs to other user or group, we are sorry!", false, None) + val NoPermission = CommonRpcErrors.forbidden("You have no permission to execute this action") } // format: ON diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/groups/GroupsServiceImpl.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/groups/GroupsServiceImpl.scala index 2256681533..8a568a5356 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/groups/GroupsServiceImpl.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/groups/GroupsServiceImpl.scala @@ -4,26 +4,30 @@ import java.time.Instant import akka.actor.ActorSystem import akka.http.scaladsl.util.FastFuture +import cats.data.Xor +import com.github.ghik.silencer.silent import im.actor.api.rpc.PeerHelpers._ import im.actor.api.rpc._ import im.actor.api.rpc.files.ApiFileLocation import im.actor.api.rpc.groups._ -import im.actor.api.rpc.misc.ResponseSeqDate +import im.actor.api.rpc.misc.{ ResponseSeq, ResponseSeqDate, ResponseVoid } import im.actor.api.rpc.peers.{ ApiGroupOutPeer, ApiUserOutPeer } import im.actor.api.rpc.sequence.ApiUpdateOptimization import im.actor.api.rpc.users.ApiUser import im.actor.concurrent.FutureExt import im.actor.server.acl.ACLUtils import im.actor.server.db.DbExtension -import im.actor.server.file.{ FileErrors, FileStorageAdapter, FileStorageExtension, ImageUtils } +import im.actor.server.dialog.DialogExtension +import im.actor.server.file.{ FileErrors, ImageUtils } import im.actor.server.group._ import im.actor.server.model.GroupInviteToken +import im.actor.server.names.GlobalNamesStorageKeyValueStorage import im.actor.server.persist.{ GroupInviteTokenRepo, GroupUserRepo } import im.actor.server.presences.GroupPresenceExtension import im.actor.server.sequence.{ SeqState, SeqStateDate, SeqUpdatesExtension } import im.actor.server.user.UserExtension import im.actor.util.ThreadLocalSecureRandom -import im.actor.util.misc.IdUtils +import im.actor.util.misc.{ IdUtils, StringUtils } import slick.dbio.DBIO import slick.driver.PostgresDriver.api._ @@ -31,12 +35,15 @@ import scala.concurrent.{ ExecutionContext, Future } final class GroupsServiceImpl(groupInviteConfig: GroupInviteConfig)(implicit actorSystem: ActorSystem) extends GroupsService { + import EntitiesHelpers._ import FileHelpers._ import FutureResultRpc._ import GroupCommands._ import IdUtils._ import ImageUtils._ + case object NoSeqStateDate extends RuntimeException("No SeqStateDate in response from group found") + override implicit val ec: ExecutionContext = actorSystem.dispatcher private val db: Database = DbExtension(actorSystem).db @@ -44,6 +51,8 @@ final class GroupsServiceImpl(groupInviteConfig: GroupInviteConfig)(implicit act private val seqUpdExt = SeqUpdatesExtension(actorSystem) private val userExt = UserExtension(actorSystem) private val groupPresenceExt = GroupPresenceExtension(actorSystem) + private val globalNamesStorage = new GlobalNamesStorageKeyValueStorage + private val dialogExt = DialogExtension(actorSystem) /** * Loading Full Groups @@ -71,13 +80,47 @@ final class GroupsServiceImpl(groupInviteConfig: GroupInviteConfig)(implicit act override protected def doHandleMakeUserAdmin(groupPeer: ApiGroupOutPeer, userPeer: ApiUserOutPeer, clientData: ClientData): Future[HandlerResult[ResponseSeqDate]] = { authorized(clientData) { implicit client ⇒ withGroupOutPeer(groupPeer) { - for { - (_, SeqStateDate(seq, state, date)) ← groupExt.makeUserAdmin(groupPeer.groupId, client.userId, client.authId, userPeer.userId) - } yield Ok(ResponseSeqDate(seq, state.toByteArray, date)) + withUserOutPeer(userPeer) { + (for { + _ ← fromFutureBoolean(GroupRpcErrors.CantGrantToBot)(userExt.getUser(userPeer.userId) map (!_.isBot)) + resp ← fromFuture(groupExt.makeUserAdmin(groupPeer.groupId, client.userId, client.authId, userPeer.userId)) + (_, SeqStateDate(seq, state, date)) = resp + } yield ResponseSeqDate(seq, state.toByteArray, date)).value + } } } } + override def doHandleDismissUserAdmin(groupPeer: ApiGroupOutPeer, userPeer: ApiUserOutPeer, clientData: ClientData): Future[HandlerResult[ResponseSeq]] = + authorized(clientData) { implicit client ⇒ + withGroupOutPeer(groupPeer) { + withUserOutPeer(userPeer) { + (for { + _ ← fromFutureBoolean(GroupRpcErrors.CantGrantToBot)(userExt.getUser(userPeer.userId) map (!_.isBot)) + seqState ← fromFuture(groupExt.dismissUserAdmin(groupPeer.groupId, client.userId, client.authId, userPeer.userId)) + } yield ResponseSeq(seqState.seq, seqState.state.toByteArray)).value + } + } + } + + override def doHandleLoadAdminSettings(groupPeer: ApiGroupOutPeer, clientData: ClientData): Future[HandlerResult[ResponseLoadAdminSettings]] = + authorized(clientData) { client ⇒ + withGroupOutPeer(groupPeer) { + for { + settings ← groupExt.loadAdminSettings(groupPeer.groupId, client.userId) + } yield Ok(ResponseLoadAdminSettings(settings)) + } + } + + override def doHandleSaveAdminSettings(groupPeer: ApiGroupOutPeer, settings: ApiAdminSettings, clientData: ClientData): Future[HandlerResult[ResponseVoid]] = + authorized(clientData) { client ⇒ + withGroupOutPeer(groupPeer) { + for { + _ ← groupExt.updateAdminSettings(groupPeer.groupId, client.userId, settings) + } yield Ok(ResponseVoid) + } + } + /** * Loading group members * @@ -94,9 +137,12 @@ final class GroupsServiceImpl(groupInviteConfig: GroupInviteConfig)(implicit act authorized(clientData) { implicit client ⇒ withGroupOutPeer(groupPeer) { for { - (userIds, nextOffset) ← groupExt.loadMembers(groupPeer.groupId, client.userId, limit, next) - members ← FutureExt.ftraverse(userIds)(userExt.getApiStruct(_, client.userId, client.authId)) - } yield Ok(ResponseLoadMembers(members.toVector map (u ⇒ ApiUserOutPeer(u.id, u.accessHash)), nextOffset)) + (members, nextOffset) ← groupExt.loadMembers(groupPeer.groupId, client.userId, limit, next) + membersAndPeers ← FutureExt.ftraverse(members) { member ⇒ + userExt.getAccessHash(member.userId, client.authId) map (hash ⇒ member → ApiUserOutPeer(member.userId, hash)) + } + (members, peers) = membersAndPeers.unzip + } yield Ok(ResponseLoadMembers(peers.toVector, nextOffset, members.toVector)) } } } @@ -107,19 +153,22 @@ final class GroupsServiceImpl(groupInviteConfig: GroupInviteConfig)(implicit act * @param groupPeer Group's peer * @param newOwner New group's owner */ - //TODO: figure out what date should be - override protected def doHandleTransferOwnership(groupPeer: ApiGroupOutPeer, newOwner: Int, clientData: ClientData): Future[HandlerResult[ResponseSeqDate]] = + override protected def doHandleTransferOwnership(groupPeer: ApiGroupOutPeer, newOwner: ApiUserOutPeer, clientData: ClientData): Future[HandlerResult[ResponseSeqDate]] = authorized(clientData) { implicit client ⇒ withGroupOutPeer(groupPeer) { - for { - SeqState(seq, state) ← groupExt.transferOwnership(groupPeer.groupId, client.userId, client.authId, newOwner) - } yield Ok(ResponseSeqDate(seq, state.toByteArray, Instant.now.toEpochMilli)) + withUserOutPeer(newOwner) { + (for { + _ ← fromFutureBoolean(GroupRpcErrors.CantGrantToBot)(userExt.getUser(newOwner.userId) map (!_.isBot)) + seqState ← fromFuture(groupExt.transferOwnership(groupPeer.groupId, client.userId, client.authId, newOwner.userId)) + } yield ResponseSeqDate( + seq = seqState.seq, + state = seqState.state.toByteArray, + date = Instant.now.toEpochMilli + )).value + } } } - case object NoSeqStateDate extends RuntimeException("No SeqStateDate in response from group found") - - // TODO: rewrite from DBIO to Future, and add access hash check override def doHandleEditGroupAvatar( groupPeer: ApiGroupOutPeer, randomId: Long, @@ -128,25 +177,26 @@ final class GroupsServiceImpl(groupInviteConfig: GroupInviteConfig)(implicit act clientData: ClientData ): Future[HandlerResult[ResponseEditGroupAvatar]] = authorized(clientData) { implicit client ⇒ - addOptimizations(optimizations) - val action = withFileLocation(fileLocation, AvatarSizeLimit) { - scaleAvatar(fileLocation.fileId) flatMap { - case Right(avatar) ⇒ - for { - UpdateAvatarAck(avatar, seqStateDate) ← DBIO.from(groupExt.updateAvatar(groupPeer.groupId, client.userId, client.authId, Some(avatar), randomId)) - SeqStateDate(seq, state, date) = seqStateDate.getOrElse(throw NoSeqStateDate) - } yield Ok(ResponseEditGroupAvatar( - avatar.get, - seq, - state.toByteArray, - date - )) - case Left(e) ⇒ - throw FileErrors.LocationInvalid + withGroupOutPeer(groupPeer) { + addOptimizations(optimizations) + val action = withFileLocation(fileLocation, AvatarSizeLimit) { + scaleAvatar(fileLocation.fileId) flatMap { + case Right(avatar) ⇒ + for { + UpdateAvatarAck(avatar, seqStateDate) ← DBIO.from(groupExt.updateAvatar(groupPeer.groupId, client.userId, client.authId, Some(avatar), randomId)) + SeqStateDate(seq, state, date) = seqStateDate.getOrElse(throw NoSeqStateDate) + } yield Ok(ResponseEditGroupAvatar( + avatar.get, + seq, + state.toByteArray, + date + )) + case Left(e) ⇒ + throw FileErrors.LocationInvalid + } } + db.run(action) } - - db.run(action) } override def doHandleRemoveGroupAvatar( @@ -183,11 +233,13 @@ final class GroupsServiceImpl(groupInviteConfig: GroupInviteConfig)(implicit act authorized(clientData) { implicit client ⇒ addOptimizations(optimizations) withGroupOutPeer(groupPeer) { - for { - SeqStateDate(seq, state, date) ← groupExt.kickUser(groupPeer.groupId, userOutPeer.userId, randomId) - } yield { - groupPresenceExt.notifyGroupUserRemoved(groupPeer.groupId, userOutPeer.userId) - Ok(ResponseSeqDate(seq, state.toByteArray, date)) + withUserOutPeer(userOutPeer) { + for { + SeqStateDate(seq, state, date) ← groupExt.kickUser(groupPeer.groupId, userOutPeer.userId, randomId) + } yield { + groupPresenceExt.notifyGroupUserRemoved(groupPeer.groupId, userOutPeer.userId) + Ok(ResponseSeqDate(seq, state.toByteArray, date)) + } } } } @@ -210,6 +262,19 @@ final class GroupsServiceImpl(groupInviteConfig: GroupInviteConfig)(implicit act } } + override def doHandleLeaveAndDelete(groupPeer: ApiGroupOutPeer, clientData: ClientData): Future[HandlerResult[ResponseSeq]] = + authorized(clientData) { implicit client ⇒ + withGroupOutPeer(groupPeer) { + for { + _ ← groupExt.leaveGroup(groupPeer.groupId, ACLUtils.randomLong()) + SeqState(seq, state) ← dialogExt.delete(client.userId, client.authId, groupPeer.asModel) + } yield { + groupPresenceExt.notifyGroupUserRemoved(groupPeer.groupId, client.userId) + Ok(ResponseSeq(seq, state.toByteArray)) + } + } + } + override def doHandleCreateGroup( randomId: Long, title: String, @@ -222,6 +287,7 @@ final class GroupsServiceImpl(groupInviteConfig: GroupInviteConfig)(implicit act addOptimizations(optimizations) withUserOutPeers(users) { val stripEntities = optimizations.contains(ApiUpdateOptimization.STRIP_ENTITIES) + val groupId = nextIntId() val typ = groupType map { case ApiGroupType.GROUP ⇒ GroupType.General @@ -241,13 +307,13 @@ final class GroupsServiceImpl(groupInviteConfig: GroupInviteConfig)(implicit act SeqStateDate(seq, state, date) = seqStateDate.getOrElse(throw NoSeqStateDate) group ← groupExt.getApiStruct(groupId, client.userId) memberIds = GroupUtils.getUserIds(group) - (apiUsers, apiPeers) ← usersOrPeers(memberIds.toVector, stripEntities) + (users, userPeers) ← usersOrPeers(memberIds.toVector, stripEntities) } yield Ok(ResponseCreateGroup( seq = seq, state = state.toByteArray, group = group, - users = apiUsers, - userPeers = apiPeers, + users = users, + userPeers = userPeers, date = date )) @@ -293,78 +359,112 @@ final class GroupsServiceImpl(groupInviteConfig: GroupInviteConfig)(implicit act override def doHandleGetGroupInviteUrl(groupPeer: ApiGroupOutPeer, clientData: ClientData): Future[HandlerResult[ResponseInviteUrl]] = authorized(clientData) { implicit client ⇒ - groupExt.getMemberIds(groupPeer.groupId) flatMap { - case (memberIds, _, _) ⇒ - if (!memberIds.contains(client.userId)) { - FastFuture.successful(Error(GroupRpcErrors.NotAMember)) - } else { - withGroupOutPeer(groupPeer) { - db.run(for { - token ← GroupInviteTokenRepo.find(groupPeer.groupId, client.userId).headOption.flatMap { - case Some(invToken) ⇒ DBIO.successful(invToken.token) - case None ⇒ - val token = ACLUtils.accessToken(ThreadLocalSecureRandom.current()) - val inviteToken = GroupInviteToken(groupPeer.groupId, client.userId, token) - for (_ ← GroupInviteTokenRepo.create(inviteToken)) yield token - } - } yield Ok(ResponseInviteUrl(genInviteUrl(token)))) - } + groupExt.getApiFullStruct(groupPeer.groupId, client.userId) flatMap { group ⇒ + val isMember = group.members.exists(_.userId == client.userId) + if (!isMember) { + FastFuture.successful(Error(GroupRpcErrors.NotAMember)) + } else { + withGroupOutPeer(groupPeer) { + for { + inviteString ← group.shortName match { + case Some(name) ⇒ FastFuture.successful(name) + case None ⇒ + db.run((GroupInviteTokenRepo.find(groupPeer.groupId, client.userId): @silent).headOption flatMap { + case Some(invToken) ⇒ DBIO.successful(invToken.token) + case None ⇒ + val token = ACLUtils.accessToken() + val inviteToken = GroupInviteToken(groupPeer.groupId, client.userId, token) + for (_ ← GroupInviteTokenRepo.create(inviteToken): @silent) yield token + }) + } + } yield Ok(ResponseInviteUrl(genInviteUrl(inviteString))) } + } } } override def doHandleJoinGroup( - urlOrToken: String, - optimizations: IndexedSeq[ApiUpdateOptimization.Value], - clientData: ClientData + joinStringOrUrl: String, + optimizations: IndexedSeq[ApiUpdateOptimization.Value], + clientData: ClientData ): Future[HandlerResult[ResponseJoinGroup]] = authorized(clientData) { implicit client ⇒ addOptimizations(optimizations) - val token = extractInviteToken(urlOrToken) val stripEntities = optimizations.contains(ApiUpdateOptimization.STRIP_ENTITIES) val action = for { - tokenInfo ← fromFutureOption(GroupRpcErrors.InvalidInviteToken)(db.run(GroupInviteTokenRepo.findByToken(token))) + joinSting ← fromOption(GroupRpcErrors.InvalidInviteUrl)(extractJoinString(joinStringOrUrl)) + joinInfo ← joinSting match { + case Xor.Left(token) ⇒ + for { + info ← fromFutureOption(GroupRpcErrors.InvalidInviteToken)(db.run(GroupInviteTokenRepo.findByToken(token): @silent)) + } yield info.groupId → Some(info.creatorId) + case Xor.Right(groupName) ⇒ + for { + groupId ← fromFutureOption(GroupRpcErrors.InvalidInviteGroup)(globalNamesStorage.getGroupId(groupName)) + } yield groupId → None + } + (groupId, optInviter) = joinInfo joinResp ← fromFuture(groupExt.joinGroup( - groupId = tokenInfo.groupId, + groupId = groupId, joiningUserId = client.userId, joiningUserAuthId = client.authId, - invitingUserId = Some(tokenInfo.creatorId) + invitingUserId = optInviter )) ((SeqStateDate(seq, state, date), userIds, randomId)) = joinResp - usersPeers ← fromFuture(usersOrPeers(userIds, stripEntities)) - groupStruct ← fromFuture(groupExt.getApiStruct(tokenInfo.groupId, client.userId)) + up ← fromFuture(usersOrPeers(userIds, stripEntities)) + (users, userPeers) = up + groupStruct ← fromFuture(groupExt.getApiStruct(groupId, client.userId)) } yield ResponseJoinGroup( groupStruct, seq, state.toByteArray, date, - usersPeers._1, + users, randomId, - usersPeers._2 + userPeers ) action.value } + override def doHandleJoinGroupByPeer(groupPeer: ApiGroupOutPeer, clientData: ClientData): Future[HandlerResult[ResponseSeq]] = + authorized(clientData) { implicit client ⇒ + withGroupOutPeer(groupPeer) { + val action = for { + apiGroup ← fromFuture(groupExt.getApiStruct(groupPeer.groupId, client.userId)) + _ ← fromBoolean(GroupRpcErrors.CantJoinGroup)(canJoin(apiGroup.permissions)) + joinResp ← fromFuture(groupExt.joinGroup( + groupId = groupPeer.groupId, + joiningUserId = client.userId, + joiningUserAuthId = client.authId, + invitingUserId = None + )) + SeqStateDate(seq, state, _) = joinResp._1 + } yield ResponseSeq(seq, state.toByteArray) + + action.value + } + } + + private def canJoin(permissions: Option[Long]) = + permissions exists (p ⇒ (p & (1 << 4)) != 0) // TODO: make wrapper around permissions + override def doHandleRevokeInviteUrl(groupPeer: ApiGroupOutPeer, clientData: ClientData): Future[HandlerResult[ResponseInviteUrl]] = authorized(clientData) { implicit client ⇒ withGroupOutPeer(groupPeer) { val token = ACLUtils.accessToken() db.run( for { - _ ← GroupInviteTokenRepo.revoke(groupPeer.groupId, client.userId) + _ ← GroupInviteTokenRepo.revoke(groupPeer.groupId, client.userId): @silent _ ← GroupInviteTokenRepo.create( GroupInviteToken(groupPeer.groupId, client.userId, token) - ) + ): @silent } yield Ok(ResponseInviteUrl(genInviteUrl(token))) ) } } - /** - * all members of group can edit group topic - */ override def doHandleEditGroupTopic( groupPeer: ApiGroupOutPeer, randomId: Long, @@ -382,9 +482,6 @@ final class GroupsServiceImpl(groupInviteConfig: GroupInviteConfig)(implicit act } } - /** - * only admin can change group's about - */ override def doHandleEditGroupAbout( groupPeer: ApiGroupOutPeer, randomId: Long, @@ -402,30 +499,51 @@ final class GroupsServiceImpl(groupInviteConfig: GroupInviteConfig)(implicit act } } - private def usersOrPeers(userIds: Vector[Int], stripEntities: Boolean)(implicit client: AuthorizedClientData): Future[(Vector[ApiUser], Vector[ApiUserOutPeer])] = - if (stripEntities) { - val users = Vector.empty[ApiUser] - val peers = Future.sequence(userIds map { userId ⇒ - userExt.getAccessHash(userId, client.authId) map (hash ⇒ ApiUserOutPeer(userId, hash)) - }) - peers map (users → _) - } else { - val users = Future.sequence(userIds map { userId ⇒ - userExt.getApiStruct(userId, client.userId, client.authId) - }) - val peers = Vector.empty[ApiUserOutPeer] - users map (_ → peers) + override def doHandleEditGroupShortName(groupPeer: ApiGroupOutPeer, shortName: Option[String], clientData: ClientData): Future[HandlerResult[ResponseSeq]] = + authorized(clientData) { client ⇒ + withGroupOutPeer(groupPeer) { + for { + SeqState(seq, state) ← groupExt.updateShortName(groupPeer.groupId, client.userId, client.authId, shortName) + } yield Ok(ResponseSeq(seq, state.toByteArray)) + } + } + + protected def doHandleDeleteGroup(groupPeer: ApiGroupOutPeer, clientData: ClientData): Future[HandlerResult[ResponseSeq]] = + authorized(clientData) { client ⇒ + withGroupOutPeer(groupPeer) { + for { + SeqState(seq, state) ← groupExt.deleteGroup(groupPeer.groupId, client.userId, client.authId) + } yield Ok(ResponseSeq(seq, state.toByteArray)) + } + } + + protected def doHandleShareHistory(groupPeer: ApiGroupOutPeer, clientData: ClientData): Future[HandlerResult[ResponseSeq]] = + authorized(clientData) { client ⇒ + withGroupOutPeer(groupPeer) { + for { + SeqState(seq, state) ← groupExt.makeHistoryShared(groupPeer.groupId, client.userId, client.authId) + } yield Ok(ResponseSeq(seq, state.toByteArray)) + } } private val inviteUriBase = s"${groupInviteConfig.baseUrl}/join/" private def genInviteUrl(token: String) = s"$inviteUriBase$token" - private def extractInviteToken(urlOrToken: String) = - if (urlOrToken.startsWith(groupInviteConfig.baseUrl)) - urlOrToken.drop(inviteUriBase.length).takeWhile(c ⇒ c != '?' && c != '#') + private def extractJoinString(urlOrTokenOrGroupName: String): Option[String Xor String] = { + val extracted = if (urlOrTokenOrGroupName.startsWith(groupInviteConfig.baseUrl)) + urlOrTokenOrGroupName.drop(inviteUriBase.length).takeWhile(c ⇒ c != '?' && c != '#') else - urlOrToken + urlOrTokenOrGroupName + + if (StringUtils.validGroupInviteToken(extracted)) { + Some(Xor.left(extracted)) + } else if (StringUtils.validGlobalName(extracted)) { + Some(Xor.right(extracted)) + } else { + None + } + } private def addOptimizations(opts: IndexedSeq[ApiUpdateOptimization.Value])(implicit client: AuthorizedClientData): Unit = seqUpdExt.addOptimizations(client.userId, client.authId, opts map (_.id)) @@ -461,11 +579,13 @@ final class GroupsServiceImpl(groupInviteConfig: GroupInviteConfig)(implicit act authorized(clientData) { implicit client ⇒ withGroupOutPeer(groupPeer) { for { - isPublic ← groupExt.isPublic(groupPeer.groupId) - result ← if (isPublic) { + // TODO: what should it be? was + // isPublic ← groupExt.isPublic(groupPeer.groupId) + isHistoryShared ← groupExt.isHistoryShared(groupPeer.groupId) + result ← if (isHistoryShared) { db.run( for { - member ← GroupUserRepo.find(groupPeer.groupId, client.userId) + member ← GroupUserRepo.find(groupPeer.groupId, client.userId): @silent response ← member match { case Some(_) ⇒ DBIO.successful(Error(GroupRpcErrors.AlreadyInvited)) case None ⇒ @@ -498,17 +618,21 @@ final class GroupsServiceImpl(groupInviteConfig: GroupInviteConfig)(implicit act ): Future[HandlerResult[ResponseMakeUserAdminObsolete]] = { authorized(clientData) { implicit client ⇒ withGroupOutPeer(groupPeer) { - for { - (members, SeqStateDate(seq, state, _)) ← groupExt.makeUserAdmin(groupPeer.groupId, client.userId, client.authId, userPeer.userId) - } yield Ok(ResponseMakeUserAdminObsolete(members, seq, state.toByteArray)) + withUserOutPeer(userPeer) { + for { + (members, SeqStateDate(seq, state, _)) ← groupExt.makeUserAdmin(groupPeer.groupId, client.userId, client.authId, userPeer.userId) + } yield Ok(ResponseMakeUserAdminObsolete(members, seq, state.toByteArray)) + } } } } override def onFailure: PartialFunction[Throwable, RpcError] = recoverCommon orElse { case GroupErrors.NotAMember ⇒ CommonRpcErrors.forbidden("Not a group member!") - case GroupErrors.NotAdmin ⇒ CommonRpcErrors.forbidden("Only admin can perform this action.") + case GroupErrors.NotAdmin ⇒ CommonRpcErrors.forbidden("Only group admin can perform this action.") + case GroupErrors.NotOwner ⇒ CommonRpcErrors.forbidden("Only group owner can perform this action.") case GroupErrors.UserAlreadyAdmin ⇒ GroupRpcErrors.UserAlreadyAdmin + case GroupErrors.UserAlreadyNotAdmin ⇒ GroupRpcErrors.UserAlreadyNotAdmin case GroupErrors.InvalidTitle ⇒ GroupRpcErrors.InvalidTitle case GroupErrors.AboutTooLong ⇒ GroupRpcErrors.AboutTooLong case GroupErrors.TopicTooLong ⇒ GroupRpcErrors.TopicTooLong @@ -517,6 +641,11 @@ final class GroupsServiceImpl(groupInviteConfig: GroupInviteConfig)(implicit act case GroupErrors.UserAlreadyInvited ⇒ GroupRpcErrors.AlreadyInvited case GroupErrors.UserAlreadyJoined ⇒ GroupRpcErrors.AlreadyJoined case GroupErrors.GroupIdAlreadyExists(_) ⇒ GroupRpcErrors.GroupIdAlreadyExists + case GroupErrors.InvalidShortName ⇒ GroupRpcErrors.InvalidShortName + case GroupErrors.ShortNameTaken ⇒ GroupRpcErrors.ShortNameTaken + case GroupErrors.NoPermission ⇒ GroupRpcErrors.NoPermission + case GroupErrors.CantLeaveGroup ⇒ GroupRpcErrors.CantLeaveGroup + case GroupErrors.UserIsBanned ⇒ GroupRpcErrors.UserIsBanned } } diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/messaging/HistoryHandlers.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/messaging/HistoryHandlers.scala index 67b4d7a80b..561f51e5be 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/messaging/HistoryHandlers.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/messaging/HistoryHandlers.scala @@ -2,23 +2,20 @@ package im.actor.server.api.rpc.service.messaging import java.time.Instant -import com.google.protobuf.wrappers.Int32Value +import akka.http.scaladsl.util.FastFuture import im.actor.api.rpc.PeerHelpers._ import im.actor.api.rpc._ -import im.actor.api.rpc.messaging.{ ApiEmptyMessage, _ } +import im.actor.api.rpc.messaging._ import im.actor.api.rpc.misc.{ ResponseSeq, ResponseVoid } -import im.actor.api.rpc.peers.{ ApiGroupOutPeer, ApiOutPeer, ApiPeerType, ApiUserOutPeer } +import im.actor.api.rpc.peers.{ ApiOutPeer, ApiPeerType } import im.actor.api.rpc.sequence.ApiUpdateOptimization import im.actor.server.dialog.HistoryUtils -import im.actor.server.group.GroupUtils -import im.actor.server.model.{ DialogObsolete, HistoryMessage, Peer, PeerType } +import im.actor.server.group.CanSendMessageInfo +import im.actor.server.model.Peer import im.actor.server.persist.contact.UserContactRepo -import im.actor.server.persist.dialog.DialogRepo -import im.actor.server.persist.{ GroupUserRepo, HistoryMessageRepo } +import im.actor.server.persist.HistoryMessageRepo import im.actor.server.sequence.SeqState -import im.actor.server.user.UserUtils import org.joda.time.DateTime -import slick.dbio import slick.driver.PostgresDriver.api._ import scala.concurrent.Future @@ -27,10 +24,12 @@ import scala.language.postfixOps trait HistoryHandlers { self: MessagingServiceImpl ⇒ - import DBIOResultRpc._ import HistoryUtils._ + import EntitiesHelpers._ import Implicits._ + private val CantDelete = Error(CommonRpcErrors.forbidden("You can't delete these messages")) + override def doHandleMessageReceived(peer: ApiOutPeer, date: Long, clientData: im.actor.api.rpc.ClientData): Future[HandlerResult[ResponseVoid]] = { authorized(clientData) { client ⇒ dialogExt.messageReceived(peer.asPeer, client.userId, date) map (_ ⇒ Ok(ResponseVoid)) @@ -44,26 +43,30 @@ trait HistoryHandlers { } override def doHandleClearChat(peer: ApiOutPeer, clientData: ClientData): Future[HandlerResult[ResponseSeq]] = - authorized(clientData) { implicit client ⇒ - val update = UpdateChatClear(peer.asPeer) + authorized(clientData) { client ⇒ + val canClearChat = peer.`type` match { + case ApiPeerType.Private | ApiPeerType.EncryptedPrivate ⇒ + FastFuture.successful(true) + case ApiPeerType.Group ⇒ + groupExt.isHistoryShared(peer.id) map (isShared ⇒ !isShared) + } - val action = (for { - _ ← fromDBIOBoolean(CommonRpcErrors.forbidden("Clearing of public chats is forbidden")) { - if (peer.`type` == ApiPeerType.Private) { - DBIO.successful(true) - } else { - DBIO.from(groupExt.isHistoryShared(peer.id)) flatMap (isHistoryShared ⇒ DBIO.successful(!isHistoryShared)) - } + canClearChat flatMap { canClear ⇒ + if (canClear) { + for { + _ ← db.run(HistoryMessageRepo.deleteAll(client.userId, peer.asModel)) + SeqState(seq, state) ← seqUpdExt.deliverClientUpdate( + client.userId, + client.authId, + update = UpdateChatClear(peer.asPeer) + ) + } yield Ok(ResponseSeq(seq, state.toByteArray)) + } else { + FastFuture.successful[HandlerResult[ResponseSeq]]( + Error(CommonRpcErrors.forbidden("Can't clear chat with shared history")) + ) } - _ ← fromDBIO(HistoryMessageRepo.deleteAll(client.userId, peer.asModel)) - seqState ← fromFuture(seqUpdExt.deliverClientUpdate( - client.userId, - client.authId, - update, - pushRules = seqUpdExt.pushRules(isFat = false, None) - )) - } yield ResponseSeq(seqState.seq, seqState.state.toByteArray)).value - db.run(action) + } } override def doHandleDeleteChat(peer: ApiOutPeer, clientData: ClientData): Future[HandlerResult[ResponseSeq]] = { @@ -81,20 +84,20 @@ trait HistoryHandlers { clientData: ClientData ): Future[HandlerResult[ResponseLoadArchived]] = authorized(clientData) { implicit client ⇒ + val stripEntities = optimizations.contains(ApiUpdateOptimization.STRIP_ENTITIES) + val loadGroupMembers = !optimizations.contains(ApiUpdateOptimization.GROUPS_V2) + for { (dialogs, nextOffset) ← dialogExt.fetchArchivedApiDialogs(client.userId, offset, limit) - (users, groups) ← db.run(getDialogsUsersGroups(dialogs.toSeq)) - } yield { - val stripEntities = optimizations.contains(ApiUpdateOptimization.STRIP_ENTITIES) - Ok(ResponseLoadArchived( - dialogs = dialogs.toVector, - nextOffset = nextOffset, - groups = if (stripEntities) Vector.empty else groups.toVector, - users = if (stripEntities) Vector.empty else users.toVector, - userPeers = users.toVector map (u ⇒ ApiUserOutPeer(u.id, u.accessHash)), - groupPeers = groups.toVector map (g ⇒ ApiGroupOutPeer(g.id, g.accessHash)) - )) - } + ((users, userPeers), (groups, groupPeers)) ← usersAndGroupsByDialogs(dialogs.toSeq, stripEntities, loadGroupMembers) + } yield Ok(ResponseLoadArchived( + dialogs = dialogs.toVector, + nextOffset = nextOffset, + groups = groups, + users = users, + userPeers = userPeers, + groupPeers = groupPeers + )) } override def doHandleLoadDialogs( @@ -104,20 +107,19 @@ trait HistoryHandlers { clientData: ClientData ): Future[HandlerResult[ResponseLoadDialogs]] = authorized(clientData) { implicit client ⇒ + val stripEntities = optimizations.contains(ApiUpdateOptimization.STRIP_ENTITIES) + val loadGroupMembers = !optimizations.contains(ApiUpdateOptimization.GROUPS_V2) + for { dialogs ← dialogExt.fetchApiDialogs(client.userId, Instant.ofEpochMilli(endDate), limit) - (users, groups) ← db.run(getDialogsUsersGroups(dialogs.toSeq)) - } yield { - val stripEntities = optimizations.contains(ApiUpdateOptimization.STRIP_ENTITIES) - - Ok(ResponseLoadDialogs( - groups = if (stripEntities) Vector.empty else groups.toVector, - users = if (stripEntities) Vector.empty else users.toVector, - dialogs = dialogs.toVector, - userPeers = users.toVector map (u ⇒ ApiUserOutPeer(u.id, u.accessHash)), - groupPeers = groups.toVector map (g ⇒ ApiGroupOutPeer(g.id, g.accessHash)) - )) - } + ((users, userPeers), (groups, groupPeers)) ← usersAndGroupsByDialogs(dialogs.toSeq, stripEntities, loadGroupMembers) + } yield Ok(ResponseLoadDialogs( + groups = groups, + users = users, + dialogs = dialogs.toVector, + userPeers = userPeers, + groupPeers = groupPeers + )) } override def doHandleLoadGroupedDialogs( @@ -125,31 +127,27 @@ trait HistoryHandlers { clientData: ClientData ): Future[HandlerResult[ResponseLoadGroupedDialogs]] = authorized(clientData) { implicit client ⇒ + val stripEntities = optimizations contains ApiUpdateOptimization.STRIP_ENTITIES + val loadGroupMembers = !optimizations.contains(ApiUpdateOptimization.GROUPS_V2) + for { dialogGroups ← dialogExt.fetchApiGroupedDialogs(client.userId) - (userIds, groupIds) = dialogGroups.view.flatMap(_.dialogs).foldLeft((Seq.empty[Int], Seq.empty[Int])) { - case ((uids, gids), dialog) ⇒ - dialog.peer.`type` match { - case ApiPeerType.Group ⇒ (uids, gids :+ dialog.peer.id) - case ApiPeerType.Private ⇒ (uids :+ dialog.peer.id, gids) - } - } - (groups, users) ← GroupUtils.getGroupsUsers(groupIds, userIds, client.userId, client.authId) + ((users, userPeers), (groups, groupPeers)) ← usersAndGroupsByShortDialogs( + dialogs = dialogGroups.flatMap(_.dialogs), + stripEntities, + loadGroupMembers + ) archivedExist ← dialogExt.fetchArchivedDialogs(client.userId, None, 1) map (_._1.nonEmpty) showInvite ← db.run(UserContactRepo.count(client.userId)) map (_ < 5) - } yield { - val stripEntities = optimizations contains ApiUpdateOptimization.STRIP_ENTITIES - - Ok(ResponseLoadGroupedDialogs( - dialogs = dialogGroups, - users = if (stripEntities) Vector.empty else users.toVector, - groups = if (stripEntities) Vector.empty else groups.toVector, - showArchived = Some(archivedExist), - showInvite = Some(showInvite), - userPeers = if (stripEntities) users.toVector map (u ⇒ ApiUserOutPeer(u.id, u.accessHash)) else Vector.empty, - groupPeers = if (stripEntities) groups.toVector map (g ⇒ ApiGroupOutPeer(g.id, g.accessHash)) else Vector.empty - )) - } + } yield Ok(ResponseLoadGroupedDialogs( + dialogs = dialogGroups, + users = users, + groups = groups, + showArchived = Some(archivedExist), + showInvite = Some(showInvite), + userPeers = userPeers, + groupPeers = groupPeers + )) } override def doHandleHideDialog(peer: ApiOutPeer, clientData: ClientData): Future[HandlerResult[ResponseDialogsOrder]] = @@ -188,9 +186,12 @@ trait HistoryHandlers { clientData: ClientData ): Future[HandlerResult[ResponseLoadHistory]] = authorized(clientData) { implicit client ⇒ - val action = withOutPeerDBIO(peer) { + withOutPeer(peer) { val modelPeer = peer.asModel - for { + val stripEntities = optimizations.contains(ApiUpdateOptimization.STRIP_ENTITIES) + val loadGroupMembers = !optimizations.contains(ApiUpdateOptimization.GROUPS_V2) + + val action = for { historyOwner ← DBIO.from(getHistoryOwner(modelPeer, client.userId)) (lastReceivedAt, lastReadAt) ← getLastReceiveReadDates(modelPeer) messageModels ← mode match { @@ -219,62 +220,61 @@ trait HistoryHandlers { case None ⇒ (msgs, uids, guids) } } - users ← DBIO.from(Future.sequence(userIds.toVector map (userExt.getApiStruct(_, client.userId, client.authId)))) - groups ← DBIO.from(Future.sequence(groupIds.toVector map (groupExt.getApiStruct(_, client.userId)))) - } yield { - val stripEntities = optimizations.contains(ApiUpdateOptimization.STRIP_ENTITIES) - - Ok(ResponseLoadHistory( - history = messages, - users = if (stripEntities) Vector.empty else users, - userPeers = users map (u ⇒ ApiUserOutPeer(u.id, u.accessHash)), - groups = if (stripEntities) Vector.empty else groups, - groupPeers = groups map (g ⇒ ApiGroupOutPeer(g.id, g.accessHash)) - )) - } + ((users, userPeers), (groups, groupPeers)) ← DBIO.from(usersAndGroupsByIds(groupIds, userIds, stripEntities, loadGroupMembers)) + } yield Ok(ResponseLoadHistory( + history = messages, + users = users, + userPeers = userPeers, + groups = groups, + groupPeers = groupPeers + )) + db.run(action) } - db.run(action) } override def doHandleDeleteMessage(outPeer: ApiOutPeer, randomIds: IndexedSeq[Long], clientData: ClientData): Future[HandlerResult[ResponseSeq]] = authorized(clientData) { implicit client ⇒ - val action = withOutPeerDBIO(outPeer) { + withOutPeer(outPeer) { val peer = outPeer.asModel - withHistoryOwner(peer, client.userId) { historyOwner ⇒ + getHistoryOwner(peer, client.userId) flatMap { historyOwner ⇒ if (isSharedUser(historyOwner)) { - HistoryMessageRepo.find(historyOwner, peer, randomIds.toSet) flatMap { messages ⇒ - if (messages.exists(_.senderUserId != client.userId)) { - DBIO.successful(Error(CommonRpcErrors.forbidden("You can only delete your own messages"))) - } else { - for { - _ ← HistoryMessageRepo.delete(historyOwner, peer, randomIds.toSet) - groupUserIds ← GroupUserRepo.findUserIds(peer.id) map (_.toSet) - SeqState(seq, state) ← DBIO.from(seqUpdExt.broadcastClientUpdate( - client.userId, - client.authId, - bcastUserIds = groupUserIds, - update = UpdateMessageDelete(outPeer.asPeer, randomIds), - pushRules = seqUpdExt.pushRules(isFat = false, None, Seq(client.authId)) - )) - } yield Ok(ResponseSeq(seq, state.toByteArray)) + for { + CanSendMessageInfo(canSend, isChannel, memberIds, _) ← groupExt.canSendMessage(peer.id, client.userId) + result ← (isChannel, canSend) match { + case (true, true) ⇒ // channel, client user is one of those who can send messages, thus he can also delete them. + deleteMessages(peer, historyOwner, randomIds, memberIds) + case (true, false) ⇒ // channel, client user can't send messages, thus he can't delete them. + FastFuture.successful(CantDelete) + case (false, _) ⇒ // not a channel group. Must check if user deletes only messages he sent. + for { + messages ← db.run(HistoryMessageRepo.find(historyOwner, peer, randomIds.toSet)) // TODO: rewrite to ids check only + res ← if (messages.forall(_.senderUserId == client.userId)) { + deleteMessages(peer, historyOwner, randomIds, memberIds) + } else { + FastFuture.successful(CantDelete) + } + } yield res } - } + } yield result } else { - for { - _ ← HistoryMessageRepo.delete(client.userId, peer, randomIds.toSet) - SeqState(seq, state) ← DBIO.from(seqUpdExt.deliverClientUpdate( - client.userId, - client.authId, - update = UpdateMessageDelete(outPeer.asPeer, randomIds), - pushRules = seqUpdExt.pushRules(isFat = false, None) - )) - } yield Ok(ResponseSeq(seq, state.toByteArray)) + deleteMessages(peer, historyOwner, randomIds, otherUsersIds = Set.empty) } } } - db.run(action) } + private def deleteMessages(peer: Peer, historyOwner: Int, randomIds: IndexedSeq[Long], otherUsersIds: Set[Int])(implicit client: AuthorizedClientData) = + for { + _ ← db.run(HistoryMessageRepo.delete(historyOwner, peer, randomIds.toSet)) + SeqState(seq, state) ← seqUpdExt.broadcastClientUpdate( + client.userId, + client.authId, + bcastUserIds = otherUsersIds, + update = UpdateMessageDelete(peer.asStruct, randomIds), + pushRules = seqUpdExt.pushRules(isFat = false, None, excludeAuthIds = Seq(client.authId)) + ) + } yield Ok(ResponseSeq(seq, state.toByteArray)) + private val MaxDateTime = new DateTime(294276, 1, 1, 0, 0) private val MaxDate = MaxDateTime.getMillis @@ -305,51 +305,4 @@ trait HistoryHandlers { } yield (new DateTime(info.lastReceivedDate.toEpochMilli), new DateTime(info.lastReadDate.toEpochMilli))) } - private def getDialogsUsersGroups(dialogs: Seq[ApiDialog])(implicit client: AuthorizedClientData) = { - val (userIds, groupIds) = dialogs.foldLeft((Set.empty[Int], Set.empty[Int])) { - case ((uacc, gacc), dialog) ⇒ - if (dialog.peer.`type` == ApiPeerType.Private) { - (uacc ++ relatedUsers(dialog.message) ++ Set(dialog.peer.id, dialog.senderUserId), gacc) - } else { - (uacc ++ relatedUsers(dialog.message) + dialog.senderUserId, gacc + dialog.peer.id) - } - } - - for { - groups ← DBIO.from(Future.sequence(groupIds map (groupExt.getApiStruct(_, client.userId)))) - groupUserIds = groups.flatMap(g ⇒ g.members.flatMap(m ⇒ Seq(m.userId, m.inviterUserId)) :+ g.creatorUserId) - users ← DBIO.from(Future.sequence((userIds ++ groupUserIds).filterNot(_ == 0) map (UserUtils.safeGetUser(_, client.userId, client.authId)))) map (_.flatten) - } yield (users, groups) - } - - private def relatedUsers(message: ApiMessage): Set[Int] = { - message match { - case ApiServiceMessage(_, extOpt) ⇒ extOpt map relatedUsers getOrElse Set.empty - case ApiTextMessage(_, mentions, _) ⇒ mentions.toSet - case ApiJsonMessage(_) ⇒ Set.empty - case _: ApiEmptyMessage ⇒ Set.empty - case _: ApiDocumentMessage ⇒ Set.empty - case _: ApiStickerMessage ⇒ Set.empty - case _: ApiUnsupportedMessage ⇒ Set.empty - case _: ApiBinaryMessage ⇒ Set.empty - case _: ApiEncryptedMessage ⇒ Set.empty - } - } - - private def relatedUsers(ext: ApiServiceEx): Set[Int] = - ext match { - case ApiServiceExContactRegistered(userId) ⇒ Set(userId) - case ApiServiceExChangedAvatar(_) ⇒ Set.empty - case ApiServiceExChangedTitle(_) ⇒ Set.empty - case ApiServiceExChangedTopic(_) ⇒ Set.empty - case ApiServiceExChangedAbout(_) ⇒ Set.empty - case ApiServiceExGroupCreated | _: ApiServiceExGroupCreated ⇒ Set.empty - case ApiServiceExPhoneCall(_) ⇒ Set.empty - case ApiServiceExPhoneMissed | _: ApiServiceExPhoneMissed ⇒ Set.empty - case ApiServiceExUserInvited(invitedUserId) ⇒ Set(invitedUserId) - case ApiServiceExUserJoined | _: ApiServiceExUserJoined ⇒ Set.empty - case ApiServiceExUserKicked(kickedUserId) ⇒ Set(kickedUserId) - case ApiServiceExUserLeft | _: ApiServiceExUserLeft ⇒ Set.empty - case _: ApiServiceExChatArchived | _: ApiServiceExChatRestored ⇒ Set.empty - } } diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/messaging/MessagingHandlers.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/messaging/MessagingHandlers.scala index 898c83f25b..83a27e0f76 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/messaging/MessagingHandlers.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/messaging/MessagingHandlers.scala @@ -3,23 +3,25 @@ package im.actor.server.api.rpc.service.messaging import akka.http.scaladsl.util.FastFuture import akka.util.Timeout import cats.data.Xor -import im.actor.api.rpc.CommonRpcErrors.IntenalError +import im.actor.api.rpc.CommonRpcErrors.InternalError import im.actor.api.rpc.{ CommonRpcErrors, _ } import im.actor.api.rpc.messaging._ import im.actor.api.rpc.misc._ import im.actor.api.rpc.peers._ import im.actor.config.ActorConfig +import im.actor.server.group.CanSendMessageInfo import im.actor.server.messaging.{ MessageParsing, MessageUpdating } +import im.actor.server.model.{ Peer, PeerType } import im.actor.server.persist.HistoryMessageRepo import scala.concurrent._ import scala.concurrent.duration._ object MessagingRpcErors { - val NotLastMessage = RpcError(400, "NOT_LAST_MESSAGE", "You are trying to edit not last message.", false, None) val NotInTimeWindow = RpcError(400, "NOT_IN_TIME_WINDOW", "You can't edit message sent more than 5 minutes age.", false, None) val NotTextMessage = RpcError(400, "NOT_TEXT_MESSAGE", "You can edit only text messages.", false, None) val NotUniqueRandomId = RpcError(400, "RANDOM_ID_NOT_UNIQUE", "", false, None) + val NotAllowedToEdit = CommonRpcErrors.forbidden("You are not allowed to edit this message") } private[messaging] trait MessagingHandlers extends PeersImplicits @@ -35,7 +37,7 @@ private[messaging] trait MessagingHandlers extends PeersImplicits private implicit val timeout: Timeout = ActorConfig.defaultTimeout // TODO: configurable - private val editTimeWindow: Long = 5.minutes.toMillis + private val editTimeWindow: Long = 1.hour.toMillis override def doHandleSendMessage( outPeer: ApiOutPeer, @@ -67,10 +69,9 @@ private[messaging] trait MessagingHandlers extends PeersImplicits withOutPeer(outPeer) { val peer = outPeer.asModel (for { - histMessage ← fromFutureOption(CommonRpcErrors.forbidden("Not allowed"))(db.run(HistoryMessageRepo.findNewestSentBy(client.userId, peer))) - _ ← fromBoolean(NotLastMessage)(histMessage.randomId == randomId) + histMessage ← fromFutureOption(NotAllowedToEdit)(getEditableHistoryMessage(peer, randomId)) _ ← fromBoolean(NotInTimeWindow)(inTimeWindow(histMessage.date.getMillis)) - apiMessage ← fromXor((e: Any) ⇒ IntenalError)(Xor.fromEither(parseMessage(histMessage.messageContentData))) + apiMessage ← fromXor((e: Any) ⇒ InternalError)(Xor.fromEither(parseMessage(histMessage.messageContentData))) _ ← fromBoolean(NotTextMessage)(apiMessage match { case _: ApiTextMessage ⇒ true case _ ⇒ false @@ -82,6 +83,29 @@ private[messaging] trait MessagingHandlers extends PeersImplicits } } + private def getEditableHistoryMessage(peer: Peer, randomId: Long)(implicit client: AuthorizedClientData) = { + def findBySender(senderId: Int) = db.run(HistoryMessageRepo.findBySender(senderId, peer, randomId).headOption) + + for { + optMessage ← peer match { + case Peer(PeerType.Private, _) ⇒ + findBySender(client.userId) + case Peer(PeerType.Group, groupId) ⇒ + for { + CanSendMessageInfo(canSend, isChannel, _, optBotId) ← groupExt.canSendMessage(groupId, client.userId) + mess ← (isChannel, canSend) match { + case (true, true) ⇒ // channel, client user is one of those who can send messages, thus he can also edit message. + (optBotId map findBySender) getOrElse FastFuture.successful(None) + case (true, false) ⇒ // channel, client user can't send messages, thus he can't edit message. + FastFuture.successful(None) + case (false, _) ⇒ // not a channel group. regular, as in case of private peer + findBySender(client.userId) + } + } yield mess + } + } yield optMessage + } + private def inTimeWindow(messageDateMillis: Long): Boolean = { (messageDateMillis + editTimeWindow) > System.currentTimeMillis } diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/messaging/MessagingServiceImpl.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/messaging/MessagingServiceImpl.scala index 5933fcec21..2b7eb0722e 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/messaging/MessagingServiceImpl.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/messaging/MessagingServiceImpl.scala @@ -30,6 +30,7 @@ final class MessagingServiceImpl(implicit protected val actorSystem: ActorSystem override def onFailure: PartialFunction[Throwable, RpcError] = { case GroupErrors.NotAMember ⇒ CommonRpcErrors.forbidden("You are not a group member.") + case GroupErrors.NotAdmin ⇒ CommonRpcErrors.forbidden("Only admin can perform this action.") case DialogErrors.MessageToSelf ⇒ CommonRpcErrors.forbidden("Sending messages to self is not allowed.") case InvalidAccessHash ⇒ CommonRpcErrors.InvalidAccessHash case DialogErrors.DialogAlreadyArchived(peer) ⇒ diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/profile/ProfileServiceImpl.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/profile/ProfileServiceImpl.scala index fa9c627bd9..f606f3c410 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/profile/ProfileServiceImpl.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/profile/ProfileServiceImpl.scala @@ -7,17 +7,15 @@ import im.actor.api.rpc.files.ApiFileLocation import im.actor.api.rpc.misc.{ ResponseBool, ResponseSeq } import im.actor.api.rpc.profile.{ ProfileService, ResponseEditAvatar } import im.actor.server.db.DbExtension -import im.actor.server.file.{ FileStorageExtension, FileErrors, FileStorageAdapter, ImageUtils } -import im.actor.server.persist.UserRepo -import im.actor.server.sequence.{ SequenceErrors, SeqState } +import im.actor.server.file.{ FileErrors, FileStorageAdapter, FileStorageExtension, ImageUtils } +import im.actor.server.names.GlobalNamesStorageKeyValueStorage +import im.actor.server.sequence.{ SeqState, SequenceErrors } import im.actor.server.social.{ SocialExtension, SocialManagerRegion } import im.actor.server.user._ -import im.actor.util.ThreadLocalSecureRandom import im.actor.util.misc.StringUtils import slick.driver.PostgresDriver.api._ import scala.concurrent.duration._ -import scala.concurrent.forkjoin.ThreadLocalRandom import scala.concurrent.{ ExecutionContext, Future } object ProfileRpcErrors { @@ -44,6 +42,7 @@ final class ProfileServiceImpl()(implicit system: ActorSystem) extends ProfileSe private val userExt = UserExtension(system) private implicit val socialRegion: SocialManagerRegion = SocialExtension(system).region private implicit val fsAdapter: FileStorageAdapter = FileStorageExtension(system).fsAdapter + private val globalNamesStorage = new GlobalNamesStorageKeyValueStorage // TODO: flatten override def doHandleEditAvatar(fileLocation: ApiFileLocation, clientData: ClientData): Future[HandlerResult[ResponseEditAvatar]] = @@ -90,12 +89,12 @@ final class ProfileServiceImpl()(implicit system: ActorSystem) extends ProfileSe override def doHandleCheckNickName(nickname: String, clientData: ClientData): Future[HandlerResult[ResponseBool]] = authorized(clientData) { implicit client ⇒ (for { - _ ← fromBoolean(ProfileRpcErrors.NicknameInvalid)(StringUtils.validUsername(nickname)) - exists ← fromFuture(db.run(UserRepo.nicknameExists(nickname.trim))) + _ ← fromBoolean(ProfileRpcErrors.NicknameInvalid)(StringUtils.validGlobalName(nickname)) + exists ← fromFuture(globalNamesStorage.exists(nickname.trim)) } yield ResponseBool(!exists)).value } - //todo: move validation inside of UserOffice + //todo: move validation inside of UserProcessor override def doHandleEditAbout(about: Option[String], clientData: ClientData): Future[HandlerResult[ResponseSeq]] = { authorized(clientData) { implicit client ⇒ (for { diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/pubgroups/PubgroupsServiceImpl.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/pubgroups/PubgroupsServiceImpl.scala deleted file mode 100644 index 4a81f7e148..0000000000 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/pubgroups/PubgroupsServiceImpl.scala +++ /dev/null @@ -1,46 +0,0 @@ -package im.actor.server.api.rpc.service.pubgroups - -import im.actor.server.group.GroupUtils -import im.actor.server.persist.{ AvatarDataRepo, GroupRepo, GroupUserRepo } - -import scala.concurrent.{ ExecutionContext, Future } -import akka.actor.ActorSystem -import slick.driver.PostgresDriver.api._ -import im.actor.api.rpc._ -import im.actor.api.rpc.pubgroups.{ ApiPublicGroup, PubgroupsService, ResponseGetPublicGroups } -import im.actor.server.ApiConversions -import im.actor.server.db.DbExtension -import im.actor.server.file.ImageUtils -import im.actor.server.model.Group -import im.actor.server.persist.contact.UserContactRepo - -class PubgroupsServiceImpl(implicit actorSystem: ActorSystem) extends PubgroupsService { - import ApiConversions._ - import ImageUtils._ - - override implicit val ec: ExecutionContext = actorSystem.dispatcher - - override def doHandleGetPublicGroups(clientData: ClientData): Future[HandlerResult[ResponseGetPublicGroups]] = { - authorized(clientData) { implicit client ⇒ - val action = for { - groups ← GroupRepo.findPublic - pubGroupStructs ← DBIO.sequence(groups map (g ⇒ getPubgroupStruct(g, client.userId))) - sorted = pubGroupStructs.sortWith((g1, g2) ⇒ g1.friendsCount >= g2.friendsCount && g1.membersCount >= g2.membersCount) - } yield Ok(ResponseGetPublicGroups(sorted.toVector)) - DbExtension(actorSystem).db.run(action) - } - } - - def getPubgroupStruct(group: Group, userId: Int)(implicit ec: ExecutionContext): DBIO[ApiPublicGroup] = { - for { - membersIds ← GroupUserRepo.findUserIds(group.id) - userContactsIds ← UserContactRepo.findNotDeletedIds(userId) - friendsCount = (membersIds intersect userContactsIds).length - groupAvatarModelOpt ← AvatarDataRepo.findByGroupId(group.id) - } yield { - ApiPublicGroup(group.id, group.accessHash, group.title, membersIds.length, friendsCount, group.about.getOrElse(""), groupAvatarModelOpt map getAvatar) - } - } - -} - diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/push/PushServiceImpl.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/push/PushServiceImpl.scala index 44caa34d3f..6d31c2007b 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/push/PushServiceImpl.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/push/PushServiceImpl.scala @@ -9,8 +9,8 @@ import im.actor.api.rpc.encryption.ApiEncryptionKey import im.actor.api.rpc.misc.ResponseVoid import im.actor.api.rpc.push.PushService import im.actor.server.db.DbExtension -import im.actor.server.model.push.{ ActorPushCredentials, ApplePushCredentials, GooglePushCredentials } -import im.actor.server.persist.push.{ ActorPushCredentialsRepo, ApplePushCredentialsRepo, GooglePushCredentialsRepo } +import im.actor.server.model.push.{ ActorPushCredentials, ApplePushCredentials, FirebasePushCredentials, GCMPushCredentials } +import im.actor.server.persist.push.{ ActorPushCredentialsRepo, ApplePushCredentialsRepo, FirebasePushCredentialsKV, GooglePushCredentialsRepo } import im.actor.server.sequence.SeqUpdatesExtension import scodec.bits.BitVector import slick.driver.PostgresDriver.api._ @@ -27,20 +27,31 @@ final class PushServiceImpl( ) extends PushService { override implicit val ec: ExecutionContext = actorSystem.dispatcher - private implicit val db: Database = DbExtension(actorSystem).db + private val db = DbExtension(actorSystem).db private implicit val seqUpdExt: SeqUpdatesExtension = SeqUpdatesExtension(actorSystem) + private val firebaseKv = new FirebasePushCredentialsKV + private val OkVoid = Ok(ResponseVoid) private val ErrWrongToken = Error(PushRpcErrors.WrongToken) - override def doHandleRegisterGooglePush(projectId: Long, token: String, clientData: ClientData): Future[HandlerResult[ResponseVoid]] = { - val creds = GooglePushCredentials(clientData.authId, projectId, token) - val action: DBIO[HandlerResult[ResponseVoid]] = for { - _ ← GooglePushCredentialsRepo.deleteByToken(token) - _ ← GooglePushCredentialsRepo.createOrUpdate(creds) - } yield OkVoid - - db.run(action.transactionally) andThen { case _ ⇒ seqUpdExt.registerGooglePushCredentials(creds) } + override def doHandleRegisterGooglePush(projectId: Long, rawToken: String, clientData: ClientData): Future[HandlerResult[ResponseVoid]] = { + val (isFCM, token) = extractToken(rawToken) + if (isFCM) { + val creds = FirebasePushCredentials(clientData.authId, projectId, token) + for { + _ ← firebaseKv.deleteByToken(token) + _ ← firebaseKv.createOrUpdate(creds) + _ ← seqUpdExt.registerFirebasePushCredentials(creds) + } yield OkVoid + } else { + val creds = GCMPushCredentials(clientData.authId, projectId, token) + db.run(for { + _ ← GooglePushCredentialsRepo.deleteByToken(token) + _ ← GooglePushCredentialsRepo.createOrUpdate(creds) + _ ← DBIO.from(seqUpdExt.registerGCMPushCredentials(creds)) + } yield OkVoid) + } } override def doHandleRegisterApplePush(apnsKey: Int, token: String, clientData: ClientData): Future[HandlerResult[ResponseVoid]] = @@ -124,8 +135,14 @@ final class PushServiceImpl( } // TODO: figure out, should user be authorized? - override protected def doHandleUnregisterGooglePush(token: String, clientData: ClientData): Future[HandlerResult[ResponseVoid]] = - seqUpdExt.unregisterGooglePushCredentials(token) map (_ ⇒ OkVoid) + override protected def doHandleUnregisterGooglePush(rawToken: String, clientData: ClientData): Future[HandlerResult[ResponseVoid]] = { + val (isFCM, token) = extractToken(rawToken) + (if (isFCM) { + seqUpdExt.unregisterFirebasePushCredentials(token) + } else { + seqUpdExt.unregisterGCMPushCredentials(token) + }) map (_ ⇒ OkVoid) + } // TODO: figure out, should user be authorized? override protected def doHandleUnregisterActorPush(endpoint: String, clientData: ClientData): Future[HandlerResult[ResponseVoid]] = @@ -157,4 +174,14 @@ final class PushServiceImpl( FastFuture.successful(ErrWrongToken) } + // FIXME: temporary hack before schema has changed + private def extractToken(token: String): (Boolean, String) = { + val fcmPref = "FCM_" + if (token.startsWith(fcmPref)) { + true → token.drop(fcmPref.length) + } else { + false → token + } + } + } diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/search/SearchServiceImpl.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/search/SearchServiceImpl.scala index c64c39571b..9b2128714d 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/search/SearchServiceImpl.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/search/SearchServiceImpl.scala @@ -12,8 +12,8 @@ import im.actor.concurrent.FutureExt import im.actor.server.db.DbExtension import im.actor.server.dialog.DialogExtension import im.actor.server.group.{ GroupExtension, GroupUtils } +import im.actor.server.names.GlobalNamesStorageKeyValueStorage import im.actor.server.persist.contact.UserContactRepo -import im.actor.server.persist.GroupRepo import im.actor.server.user.UserExtension import scala.concurrent.{ ExecutionContext, Future } @@ -22,8 +22,12 @@ class SearchServiceImpl(implicit system: ActorSystem) extends SearchService { override implicit protected val ec: ExecutionContext = system.dispatcher protected val db = DbExtension(system).db - protected val userExt = UserExtension(system) - protected val groupExt = GroupExtension(system) + + private val userExt = UserExtension(system) + private val groupExt = GroupExtension(system) + private val globalNamesStorage = new GlobalNamesStorageKeyValueStorage + + private val EmptyResult = ResponsePeerSearch(Vector.empty, Vector.empty, Vector.empty, Vector.empty, Vector.empty) override def doHandlePeerSearch( query: IndexedSeq[ApiSearchCondition], @@ -36,11 +40,19 @@ class SearchServiceImpl(implicit system: ActorSystem) extends SearchService { case ((pts, txts), ApiSearchPeerTypeCondition(pt)) ⇒ (pts + pt, txts) case ((pts, txts), _) ⇒ (pts, txts) } + val peerTypesSorted = peerTypes.toVector.sortBy(_.id) texts.toList match { - case text :: Nil ⇒ searchResult(peerTypes.toVector, Some(text), optimizations) - case Nil ⇒ searchResult(peerTypes.toVector, None, optimizations) - case _ ⇒ FastFuture.successful(Error(RpcError(400, "INVALID_QUERY", "Invalid query.", canTryAgain = false, None))) + case text :: Nil if text.length < 3 ⇒ + FastFuture.successful(Ok(EmptyResult)) + case text :: Nil ⇒ + val tps = if (peerTypes.isEmpty) + Vector(ApiSearchPeerType.Groups, ApiSearchPeerType.Contacts, ApiSearchPeerType.Public) + else + peerTypesSorted + searchResult(tps, Some(text), optimizations) + case Nil ⇒ searchResult(peerTypesSorted, None, optimizations) + case _ ⇒ FastFuture.successful(Error(RpcError(400, "INVALID_QUERY", "Invalid query.", canTryAgain = false, None))) } } } @@ -64,28 +76,37 @@ class SearchServiceImpl(implicit system: ActorSystem) extends SearchService { text: Option[String], optimizations: IndexedSeq[ApiUpdateOptimization.Value] )(implicit client: AuthorizedClientData): Future[HandlerResult[ResponsePeerSearch]] = { + val stripEntities = optimizations.contains(ApiUpdateOptimization.STRIP_ENTITIES) + val loadGroupMembers = !optimizations.contains(ApiUpdateOptimization.GROUPS_V2) + for { results ← FutureExt.ftraverse(pts)(search(_, text)).map(_.reduce(_ ++ _)) - (groupIds, userIds) = results.view.map(_.peer).foldLeft(Vector.empty[Int], Vector.empty[Int]) { - case ((gids, uids), ApiPeer(pt, pid)) ⇒ - pt match { - case ApiPeerType.Private ⇒ (gids, uids :+ pid) - case ApiPeerType.Group ⇒ (gids :+ pid, uids) + (groupIds, userIds, searchResults) = (results foldLeft (Set.empty[Int], Set.empty[Int], Vector.empty[ApiPeerSearchResult])) { + case (acc @ (gids, uids, rslts), found @ ApiPeerSearchResult(peer, _)) ⇒ + if (rslts.exists(_.peer == peer)) { + acc + } else { + peer.`type` match { + case ApiPeerType.Private ⇒ (gids, uids + peer.id, rslts :+ found) + case ApiPeerType.Group ⇒ (gids + peer.id, uids, rslts :+ found) + } } } - (groups, users) ← GroupUtils.getGroupsUsers(groupIds, userIds, client.userId, client.authId) + ((users, userPeers), (groups, groupPeers)) ← EntitiesHelpers.usersAndGroupsByIds(groupIds, userIds, stripEntities, loadGroupMembers) } yield { - val stripEntities = optimizations.contains(ApiUpdateOptimization.STRIP_ENTITIES) + Ok(ResponsePeerSearch( - searchResults = results, - users = if (stripEntities) Vector.empty else users.toVector, - groups = if (stripEntities) Vector.empty else groups.toVector, - userPeers = users.toVector map (u ⇒ ApiUserOutPeer(u.id, u.accessHash)), - groupPeers = groups.toVector map (g ⇒ ApiGroupOutPeer(g.id, g.accessHash)) + searchResults = searchResults, + users = users, + groups = groups, + userPeers = userPeers, + groupPeers = groupPeers )) } } + type PeerAndMatchString = (ApiPeer, String) + private def search(pt: ApiSearchPeerType.Value, text: Option[String])(implicit clientData: AuthorizedClientData): Future[IndexedSeq[ApiPeerSearchResult]] = { pt match { case ApiSearchPeerType.Contacts ⇒ @@ -94,87 +115,111 @@ class SearchServiceImpl(implicit system: ActorSystem) extends SearchService { } yield users map result case ApiSearchPeerType.Groups ⇒ for { - groups ← searchGroups(text) - } yield groups map (result(_, isPublic = false)) + groups ← searchLocalGroups(text) + } yield groups map result case ApiSearchPeerType.Public ⇒ + val usersFull = searchGlobalUsers(text) + val usersPrefix = searchGlobalUsersPrefix(text) + val groupsPrefix = searchGlobalGroupsPrefix(text) for { - groups ← searchPublic(text) - } yield groups map (result(_, isPublic = true)) + uf ← usersFull + up ← usersPrefix + gp ← groupsPrefix + } yield (uf map result) ++ (up map result) ++ (gp map result) } } - private def result(apiUser: ApiUser): ApiPeerSearchResult = + private def result(peer: ApiPeer): ApiPeerSearchResult = ApiPeerSearchResult( - peer = ApiPeer(ApiPeerType.Private, apiUser.id), - title = apiUser.localName.getOrElse(apiUser.name), - description = None, - membersCount = None, - dateCreated = None, - creator = None, - isPublic = None, - isJoined = None + peer = peer, + optMatchString = None ) - private def result(apiGroup: ApiGroup, isPublic: Boolean): ApiPeerSearchResult = + private def result(peerAndMatch: PeerAndMatchString): ApiPeerSearchResult = ApiPeerSearchResult( - peer = ApiPeer(ApiPeerType.Group, apiGroup.id), - title = apiGroup.title, - description = apiGroup.about, - membersCount = Some(apiGroup.members.size), - dateCreated = Some(apiGroup.createDate), - creator = Some(apiGroup.creatorUserId), - isPublic = Some(isPublic), - isJoined = apiGroup.isMember + peer = peerAndMatch._1, + optMatchString = Some(peerAndMatch._2) ) - private def searchContacts(text: Option[String])(implicit client: AuthorizedClientData): Future[IndexedSeq[ApiUser]] = { + private def userPeersWithoutSelf(userIds: Seq[Int])(implicit client: AuthorizedClientData): Vector[ApiPeer] = + (userIds collect { + case userId if userId != client.userId ⇒ ApiPeer(ApiPeerType.Private, userId) + }).toVector + + // search users by full phone number, email or nickname + private def searchGlobalUsers(text: Option[String])(implicit client: AuthorizedClientData): Future[IndexedSeq[ApiPeer]] = { + text map { query ⇒ + userExt.findUserIds(query) map userPeersWithoutSelf + } getOrElse FastFuture.successful(Vector.empty) + } + + // search users by nickname prefix + private def searchGlobalUsersPrefix(text: Option[String])(implicit client: AuthorizedClientData): Future[IndexedSeq[PeerAndMatchString]] = { + text map { query ⇒ + globalNamesStorage.userIdsByPrefix(normName(query)) map { results ⇒ + results collect { + case (userId, nickName) if userId != client.userId ⇒ + ApiPeer(ApiPeerType.Private, userId) → s"@$nickName" + } + } + } getOrElse FastFuture.successful(Vector.empty) + } + + // find groups by global name prefix + private def searchGlobalGroupsPrefix(text: Option[String]): Future[IndexedSeq[PeerAndMatchString]] = { + text map { query ⇒ + globalNamesStorage.groupIdsByPrefix(normName(query)) map { results ⇒ + results map { + case (groupId, globalName) ⇒ + ApiPeer(ApiPeerType.Group, groupId) → s"@$globalName" + } + } + } getOrElse FastFuture.successful(Vector.empty) + } + + private def normName(n: String) = if (n.startsWith("@")) n.drop(1) else n + + private def searchContacts(text: Option[String])(implicit client: AuthorizedClientData): Future[IndexedSeq[ApiPeer]] = { for { userIds ← db.run(UserContactRepo.findContactIdsActive(client.userId)) users ← FutureExt.ftraverse(userIds)(userExt.getApiStruct(_, client.userId, client.authId)) } yield filterUsers(users.toVector, text) } - private def filterUsers(users: IndexedSeq[ApiUser], textOpt: Option[String]): IndexedSeq[ApiUser] = + private def filterUsers(users: IndexedSeq[ApiUser], textOpt: Option[String]): IndexedSeq[ApiPeer] = textOpt match { case Some(text) ⇒ val lotext = text.toLowerCase users filter { user ⇒ user.name.toLowerCase.contains(lotext) || user.localName.exists(_.toLowerCase.contains(lotext)) + } map { u ⇒ + ApiPeer(ApiPeerType.Private, u.id) } - case None ⇒ users + case None ⇒ users map { u ⇒ ApiPeer(ApiPeerType.Private, u.id) } } - // TODO: rewrite it using async - private def searchGroups(text: Option[String])(implicit client: AuthorizedClientData): Future[IndexedSeq[ApiGroup]] = { + private def searchLocalGroups(text: Option[String])(implicit client: AuthorizedClientData): Future[IndexedSeq[ApiPeer]] = { for { ids ← DialogExtension(system).fetchGroupedDialogs(client.userId) map (_.filter(_.typ.isGroups).flatMap(_.dialogs.map(_.getPeer.id))) - groupOpts ← FutureExt.ftraverse(ids) { id ⇒ - groupExt.isPublic(id) flatMap { isPublic ⇒ - if (isPublic) FastFuture.successful(None) - else groupExt.getApiStruct(id, client.userId).map(Some(_)) - } + groups ← FutureExt.ftraverse(ids) { id ⇒ + groupExt.getApiStruct(id, client.userId) } - } yield filterGroups(groupOpts.flatten.toVector, text) - } - - private def searchPublic(text: Option[String])(implicit client: AuthorizedClientData): Future[IndexedSeq[ApiGroup]] = { - for { - groups ← db.run(GroupRepo.findPublic) - groups ← FutureExt.ftraverse(groups)(g ⇒ groupExt.getApiStruct(g.id, client.userId)) } yield filterGroups(groups.toVector, text) } - private def filterGroups(groups: IndexedSeq[ApiGroup], textOpt: Option[String]): IndexedSeq[ApiGroup] = { + private def filterGroups(groups: IndexedSeq[ApiGroup], textOpt: Option[String]): IndexedSeq[ApiPeer] = { textOpt match { case Some(text) ⇒ - groups.view.filter { group ⇒ + groups filter { group ⇒ val lotext = text.toLowerCase group.title.toLowerCase.contains(lotext) || group.about.exists(_.toLowerCase.contains(lotext)) || group.theme.exists(_.toLowerCase.contains(lotext)) - }.force - case None ⇒ groups + } map { g ⇒ + ApiPeer(ApiPeerType.Group, g.id) + } + case None ⇒ groups map { g ⇒ ApiPeer(ApiPeerType.Group, g.id) } } } } diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/sequence/SequenceServiceImpl.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/sequence/SequenceServiceImpl.scala index 96728a2905..1fcd90b4d1 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/sequence/SequenceServiceImpl.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/sequence/SequenceServiceImpl.scala @@ -14,6 +14,7 @@ import akka.http.scaladsl.util.FastFuture import im.actor.api.rpc._ import im.actor.api.rpc.misc.{ ResponseSeq, ResponseVoid } import im.actor.api.rpc.peers.{ ApiGroupOutPeer, ApiUserOutPeer } +import im.actor.concurrent.FutureExt import im.actor.server.db.DbExtension import im.actor.server.group.{ GroupExtension, GroupUtils } import im.actor.server.sequence.{ Difference, SeqState, SeqUpdatesExtension } @@ -27,12 +28,14 @@ final class SequenceServiceImpl(config: SequenceServiceConfig)( actorSystem: ActorSystem ) extends SequenceService { import FutureResultRpc._ + import PeerHelpers._ protected override implicit val ec: ExecutionContext = actorSystem.dispatcher private val log = Logging(actorSystem, getClass) private val db: Database = DbExtension(actorSystem).db - private implicit val seqUpdExt: SeqUpdatesExtension = SeqUpdatesExtension(actorSystem) + private val seqUpdExt = SeqUpdatesExtension(actorSystem) + private val groupExt = GroupExtension(actorSystem) private val maxDifferenceSize: Long = config.maxDifferenceSize @@ -97,8 +100,17 @@ final class SequenceServiceImpl(config: SequenceServiceConfig)( ): Future[HandlerResult[ResponseGetReferencedEntitites]] = authorized(clientData) { client ⇒ (for { + // check access hash only for private groups. + // No need to check access hash for public groups. + privateGroups ← fromFuture((groups foldLeft FastFuture.successful(Vector.empty[ApiGroupOutPeer])) { + case (accFu, el) ⇒ + for { + acc ← accFu + isShared ← groupExt.isHistoryShared(el.groupId) + } yield if (isShared) acc else acc :+ el + }) _ ← fromFutureBoolean(CommonRpcErrors.InvalidAccessHash)(ACLUtils.checkUserOutPeers(users, client.authId)) - _ ← fromFutureBoolean(CommonRpcErrors.InvalidAccessHash)(ACLUtils.checkGroupOutPeers(groups)) + _ ← fromFutureBoolean(CommonRpcErrors.InvalidAccessHash)(ACLUtils.checkGroupOutPeers(privateGroups)) res ← fromFuture(GroupUtils.getGroupsUsers( groups map (_.groupId), users map (_.userId), client.userId, client.authId @@ -135,22 +147,35 @@ final class SequenceServiceImpl(config: SequenceServiceConfig)( } } - override def doHandleSubscribeToGroupOnline(groups: IndexedSeq[ApiGroupOutPeer], clientData: ClientData): Future[HandlerResult[ResponseVoid]] = { - FastFuture.successful(Ok(ResponseVoid)) andThen { - case _ ⇒ - // FIXME: #security check access hashes - sessionRegion.ref ! SessionEnvelope(clientData.authId, clientData.sessionId) - .withSubscribeToGroupOnline(SubscribeToGroupOnline(groups.map(_.groupId))) + override def doHandleSubscribeToGroupOnline(groups: IndexedSeq[ApiGroupOutPeer], clientData: ClientData): Future[HandlerResult[ResponseVoid]] = + withGroupOutPeers(groups) { + FastFuture.successful(Ok(ResponseVoid)) andThen { + case _ ⇒ + getNonChannelsIds(groups) foreach { groupIds ⇒ + sessionRegion.ref ! SessionEnvelope(clientData.authId, clientData.sessionId) + .withSubscribeToGroupOnline(SubscribeToGroupOnline(groupIds)) + } + } } - } - override def doHandleSubscribeFromGroupOnline(groups: IndexedSeq[ApiGroupOutPeer], clientData: ClientData): Future[HandlerResult[ResponseVoid]] = { - FastFuture.successful(Ok(ResponseVoid)) andThen { - case _ ⇒ - // FIXME: #security check access hashes - sessionRegion.ref ! SessionEnvelope(clientData.authId, clientData.sessionId) - .withSubscribeFromGroupOnline(SubscribeFromGroupOnline(groups.map(_.groupId))) + override def doHandleSubscribeFromGroupOnline(groups: IndexedSeq[ApiGroupOutPeer], clientData: ClientData): Future[HandlerResult[ResponseVoid]] = + withGroupOutPeers(groups) { + FastFuture.successful(Ok(ResponseVoid)) andThen { + case _ ⇒ + getNonChannelsIds(groups) foreach { groupIds ⇒ + sessionRegion.ref ! SessionEnvelope(clientData.authId, clientData.sessionId) + .withSubscribeFromGroupOnline(SubscribeFromGroupOnline(groupIds)) + } + + } } + + // TODO: add non deleted check too. + private def getNonChannelsIds(groups: Seq[ApiGroupOutPeer]): Future[Seq[Int]] = { + FutureExt.ftraverse(groups) { + case ApiGroupOutPeer(groupId, _) ⇒ + groupExt.isChannel(groupId) map (isChannel ⇒ if (isChannel) None else Some(groupId)) + } map (_.flatten) } //TODO: remove @@ -185,7 +210,7 @@ final class SequenceServiceImpl(config: SequenceServiceConfig)( FastFuture.successful((Vector.empty, Vector.empty)) else for { - groups ← Future.sequence(groupIds.toVector map (GroupExtension(actorSystem).getApiStruct(_, client.userId))) + groups ← Future.sequence(groupIds.toVector map (groupExt.getApiStruct(_, client.userId))) // TODO: #perf optimize collection operations allUserIds = userIds ++ groups.foldLeft(Vector.empty[Int]) { (ids, g) ⇒ ids ++ g.members.flatMap(m ⇒ Vector(m.userId, m.inviterUserId)) :+ g.creatorUserId } users ← Future.sequence(allUserIds.toVector map (UserUtils.safeGetUser(_, client.userId, client.authId))) map (_.flatten) diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/weak/WeakServiceImpl.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/weak/WeakServiceImpl.scala index 5b00d19898..08fb4fc9ae 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/weak/WeakServiceImpl.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/weak/WeakServiceImpl.scala @@ -8,7 +8,7 @@ import im.actor.api.rpc.peers.{ ApiOutPeer, ApiPeer, ApiPeerType } import im.actor.api.rpc.weak._ import im.actor.concurrent.FutureExt import im.actor.server.db.DbExtension -import im.actor.server.group.GroupExtension +import im.actor.server.group.{ CanSendMessageInfo, GroupExtension } import im.actor.server.presences.PresenceExtension import im.actor.server.sequence.WeakUpdatesExtension import slick.driver.PostgresDriver.api._ @@ -27,6 +27,11 @@ class WeakServiceImpl(implicit actorSystem: ActorSystem) extends WeakService { override def doHandleTyping(peer: ApiOutPeer, typingType: ApiTypingType.ApiTypingType, clientData: ClientData): Future[HandlerResult[ResponseVoid]] = { authorized(clientData) { client ⇒ peer.`type` match { + case ApiPeerType.EncryptedPrivate ⇒ + val update = UpdateTyping(ApiPeer(ApiPeerType.EncryptedPrivate, client.userId), client.userId, typingType) + val reduceKey = weakUpdatesExt.reduceKey(update.header, update.peer) + + weakUpdatesExt.broadcastUserWeakUpdate(peer.id, update, reduceKey = Some(reduceKey)) case ApiPeerType.Private ⇒ val update = UpdateTyping(ApiPeer(ApiPeerType.Private, client.userId), client.userId, typingType) val reduceKey = weakUpdatesExt.reduceKey(update.header, update.peer) @@ -37,8 +42,11 @@ class WeakServiceImpl(implicit actorSystem: ActorSystem) extends WeakService { val reduceKey = weakUpdatesExt.reduceKey(update.header, update.peer, client.userId) for { - (memberIds, _, _) ← groupExt.getMemberIds(peer.id) - _ ← FutureExt.ftraverse(memberIds filterNot (_ == client.userId))(weakUpdatesExt.broadcastUserWeakUpdate(_, update, Some(reduceKey))) + CanSendMessageInfo(_, isChannel, memberIds, _) ← groupExt.canSendMessage(peer.id, client.userId) + _ ← if (isChannel) + FastFuture.successful(()) + else + FutureExt.ftraverse((memberIds - client.userId).toSeq)(weakUpdatesExt.broadcastUserWeakUpdate(_, update, Some(reduceKey))) } yield () } @@ -74,17 +82,26 @@ class WeakServiceImpl(implicit actorSystem: ActorSystem) extends WeakService { override def doHandleStopTyping(peer: ApiOutPeer, typingType: ApiTypingType.ApiTypingType, clientData: ClientData): Future[HandlerResult[ResponseVoid]] = { authorized(clientData) { client ⇒ peer.`type` match { + case ApiPeerType.EncryptedPrivate ⇒ + val update = UpdateTypingStop(ApiPeer(ApiPeerType.EncryptedPrivate, client.userId), client.userId, typingType) + val reduceKey = weakUpdatesExt.reduceKey(update.header, update.peer) + + weakUpdatesExt.broadcastUserWeakUpdate(peer.id, update, reduceKey = Some(reduceKey)) case ApiPeerType.Private ⇒ val update = UpdateTypingStop(ApiPeer(ApiPeerType.Private, client.userId), client.userId, typingType) val reduceKey = weakUpdatesExt.reduceKey(update.header, update.peer) - weakUpdatesExt.broadcastUserWeakUpdate(peer.id, update, Some(reduceKey)) + + weakUpdatesExt.broadcastUserWeakUpdate(peer.id, update, reduceKey = Some(reduceKey)) case ApiPeerType.Group ⇒ val update = UpdateTypingStop(ApiPeer(ApiPeerType.Group, peer.id), client.userId, typingType) val reduceKey = weakUpdatesExt.reduceKey(update.header, update.peer) for { - (memberIds, _, _) ← groupExt.getMemberIds(peer.id) - _ ← FutureExt.ftraverse(memberIds filterNot (_ == client.userId))(weakUpdatesExt.broadcastUserWeakUpdate(_, update, Some(reduceKey))) + CanSendMessageInfo(_, isChannel, memberIds, _) ← groupExt.canSendMessage(peer.id, client.userId) + _ ← if (isChannel) + FastFuture.successful(()) + else + FutureExt.ftraverse((memberIds - client.userId).toSeq)(weakUpdatesExt.broadcastUserWeakUpdate(_, update, Some(reduceKey))) } yield () } diff --git a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/webhooks/IntegrationsServiceImpl.scala b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/webhooks/IntegrationsServiceImpl.scala index cfa1ca5f71..ddaec3d85a 100644 --- a/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/webhooks/IntegrationsServiceImpl.scala +++ b/actor-server/actor-rpc-api/src/main/scala/im/actor/server/api/rpc/service/webhooks/IntegrationsServiceImpl.scala @@ -9,7 +9,7 @@ import im.actor.api.rpc.integrations.{ IntegrationsService, ResponseIntegrationT import im.actor.api.rpc.peers.{ ApiOutPeer, ApiPeerType } import im.actor.server.api.rpc.service.webhooks.IntegrationServiceHelpers._ import im.actor.server.db.DbExtension -import im.actor.server.group.GroupErrors.{ NotAMember, NotAdmin } +import im.actor.server.group.GroupErrors.{ NoPermission, NotAMember, NotAdmin } import im.actor.server.group.GroupExtension import slick.driver.PostgresDriver.api._ @@ -55,8 +55,9 @@ class IntegrationsServiceImpl(baseUri: String)(implicit actorSystem: ActorSystem } override def onFailure: PartialFunction[Throwable, RpcError] = { - case NotAdmin ⇒ CommonRpcErrors.forbidden("Only admin can perform this action.") - case NotAMember ⇒ CommonRpcErrors.forbidden("You are not a group member.") + case NotAdmin ⇒ CommonRpcErrors.forbidden("Only admin can perform this action.") + case NotAMember ⇒ CommonRpcErrors.forbidden("You are not a group member.") + case NoPermission ⇒ CommonRpcErrors.forbidden("You have no permission to execute this action") } } diff --git a/actor-server/actor-runtime/src/main/scala/im/actor/concurrent/FutureResult.scala b/actor-server/actor-runtime/src/main/scala/im/actor/concurrent/FutureResult.scala index 894d92329a..6b5ae18d93 100644 --- a/actor-server/actor-runtime/src/main/scala/im/actor/concurrent/FutureResult.scala +++ b/actor-server/actor-runtime/src/main/scala/im/actor/concurrent/FutureResult.scala @@ -3,7 +3,7 @@ package im.actor.concurrent import akka.http.scaladsl.util.FastFuture import cats.data.Xor._ import cats.data.{ Xor, XorT } -import cats.std.{ EitherInstances, FutureInstances } +import cats.instances.{ EitherInstances, FutureInstances } import cats.syntax.all._ import scala.concurrent.{ ExecutionContext, Future } diff --git a/actor-server/actor-runtime/src/main/scala/im/actor/extension/InternalExtensions.scala b/actor-server/actor-runtime/src/main/scala/im/actor/extension/InternalExtensions.scala index 0eec01518c..c0ad8b8930 100644 --- a/actor-server/actor-runtime/src/main/scala/im/actor/extension/InternalExtensions.scala +++ b/actor-server/actor-runtime/src/main/scala/im/actor/extension/InternalExtensions.scala @@ -10,10 +10,6 @@ trait InternalExtension object InternalExtensions { - def getId(path: String, name: String) = { - ActorConfig.load().getInt(s"$path.$name.id") - } - def extensions(path: String): Map[Int, String] = { val extConfig = ActorConfig.load().getConfig(path) val extensionsKeys = extConfig.root.keys @@ -26,4 +22,4 @@ object InternalExtensions { val constructor = Class.forName(extensionFQN).getConstructors()(0) constructor.newInstance(system, data).asInstanceOf[T] } -} \ No newline at end of file +} diff --git a/actor-server/actor-runtime/src/main/scala/im/actor/util/misc/StringUtils.scala b/actor-server/actor-runtime/src/main/scala/im/actor/util/misc/StringUtils.scala index 5f5395ab37..728243ad6e 100644 --- a/actor-server/actor-runtime/src/main/scala/im/actor/util/misc/StringUtils.scala +++ b/actor-server/actor-runtime/src/main/scala/im/actor/util/misc/StringUtils.scala @@ -14,6 +14,8 @@ object StringUtils { private val usernamePattern = Pattern.compile("""^[0-9a-zA-Z_]{5,32}""", Pattern.UNICODE_CHARACTER_CLASS) + private val sha256Pattern = Pattern.compile("^[A-Fa-f0-9]{64}$", Pattern.UNICODE_CHARACTER_CLASS) + private val transliterator = Transliterator.getInstance("Latin; Latin-ASCII") def utfToHexString(s: String): String = { s.map(ch ⇒ f"${ch.toInt}%04X").mkString } @@ -26,18 +28,20 @@ object StringUtils { def nonEmptyString(s: String): NonEmptyList[String] Xor String = { val trimmed = s.trim - if (trimmed.isEmpty) NonEmptyList("Should be nonempty").left else trimmed.right + if (trimmed.isEmpty) NonEmptyList.of("Should be nonempty").left else trimmed.right } def printableString(s: String): NonEmptyList[String] Xor String = { val p = Pattern.compile("\\p{Print}+", Pattern.UNICODE_CHARACTER_CLASS) - if (p.matcher(s).matches) s.right else NonEmptyList("Should contain printable characters only").left + if (p.matcher(s).matches) s.right else NonEmptyList.of("Should contain printable characters only").left } def validName(n: String): NonEmptyList[String] Xor String = nonEmptyString(n).flatMap(printableString) - def validUsername(username: String): Boolean = usernamePattern.matcher(username.trim).matches + def validGlobalName(username: String): Boolean = usernamePattern.matcher(username.trim).matches + + def validGroupInviteToken(token: String): Boolean = sha256Pattern.matcher(token.trim).matches def normalizeUsername(username: String): Option[String] = { val trimmed = username.trim diff --git a/actor-server/actor-server-sdk/src/main/scala/im/actor/server/ActorServer.scala b/actor-server/actor-server-sdk/src/main/scala/im/actor/server/ActorServer.scala index 230caa9d38..10a1a5c10b 100644 --- a/actor-server/actor-server-sdk/src/main/scala/im/actor/server/ActorServer.scala +++ b/actor-server/actor-server-sdk/src/main/scala/im/actor/server/ActorServer.scala @@ -22,7 +22,6 @@ import im.actor.server.api.rpc.service.groups.{ GroupInviteConfig, GroupsService import im.actor.server.api.rpc.service.messaging.MessagingServiceImpl import im.actor.server.api.rpc.service.privacy.PrivacyServiceImpl import im.actor.server.api.rpc.service.profile.ProfileServiceImpl -import im.actor.server.api.rpc.service.pubgroups.PubgroupsServiceImpl import im.actor.server.api.rpc.service.push.PushServiceImpl import im.actor.server.api.rpc.service.sequence.{ SequenceServiceConfig, SequenceServiceImpl } import im.actor.server.api.rpc.service.stickers.StickersServiceImpl @@ -195,9 +194,6 @@ final case class ActorServerBuilder(defaultConfig: Config = ConfigFactory.empty( system.log.debug("Starting GroupsService") val groupsService = new GroupsServiceImpl(groupInviteConfig) - system.log.debug("Starting PubgroupsService") - val pubgroupsService = new PubgroupsServiceImpl - system.log.debug("Starting SequenceService") val sequenceService = new SequenceServiceImpl(sequenceConfig) @@ -251,7 +247,6 @@ final case class ActorServerBuilder(defaultConfig: Config = ConfigFactory.empty( contactsService, messagingService, groupsService, - pubgroupsService, sequenceService, weakService, usersService, diff --git a/actor-server/actor-tests/src/test/scala/im/actor/server/api/http/HttpApiFrontendSpec.scala b/actor-server/actor-tests/src/test/scala/im/actor/server/api/http/HttpApiFrontendSpec.scala index 031d453358..bb5ad0d311 100644 --- a/actor-server/actor-tests/src/test/scala/im/actor/server/api/http/HttpApiFrontendSpec.scala +++ b/actor-server/actor-tests/src/test/scala/im/actor/server/api/http/HttpApiFrontendSpec.scala @@ -56,6 +56,8 @@ final class HttpApiFrontendSpec "Groups handler" should "respond with JSON message to group invite info with correct invite token" in t.groupInvitesOk() + it should "respond with JSON message without inviter, when we join via group short name" in t.groupInvitesShortName() + it should "respond with JSON message with avatar full links to group invite info with correct invite token" in t.groupInvitesAvatars1() it should "respond with JSON message with avatar partial links to group invite info with correct invite token" in t.groupInvitesAvatars2() @@ -316,6 +318,22 @@ final class HttpApiFrontendSpec } } + def groupInvitesShortName() = { + val shortName = "division" + whenReady(groupExt.updateShortName(groupOutPeer.groupId, user1.id, authId1, Some(shortName))) { _ ⇒ + val request = HttpRequest( + method = GET, + uri = s"${config.baseUri}/v1/groups/invites/$shortName" + ) + val resp = singleRequest(request).futureValue + resp.status shouldEqual OK + val body = resp.entity.asString + val response = Json.parse(body) + (response \ "group" \ "title").as[String] shouldEqual groupName + (response \ "inviter" \ "name").toOption shouldEqual None + } + } + def groupInvitesAvatars1() = { val avatarData = Files.readAllBytes(Paths.get(getClass.getResource("/valid-avatar.jpg").toURI)) val fileLocation = db.run(fsAdapter.uploadFile(UnsafeFileName("avatar"), avatarData)).futureValue diff --git a/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/MessagingServiceSpec.scala b/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/MessagingServiceSpec.scala index a0d59662b2..aa7148ab74 100644 --- a/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/MessagingServiceSpec.scala +++ b/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/MessagingServiceSpec.scala @@ -154,9 +154,6 @@ class MessagingServiceSpec implicit val clientData = clientData2 expectUpdate(classOf[UpdateChatGroupsChanged])(identity) expectUpdate(classOf[UpdateMessage])(identity) - expectUpdate(classOf[UpdateCountersChanged]) { upd ⇒ - upd.counters.globalCounter shouldEqual Some(1) - } } } } diff --git a/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/PubgroupsServiceSpec.scala b/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/PubgroupsServiceSpec.scala deleted file mode 100644 index ae1f6ac6a5..0000000000 --- a/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/PubgroupsServiceSpec.scala +++ /dev/null @@ -1,129 +0,0 @@ -package im.actor.server.api.rpc.service - -import im.actor.api.rpc._ -import im.actor.api.rpc.pubgroups.ResponseGetPublicGroups -import im.actor.server._ -import im.actor.server.acl.ACLUtils -import im.actor.server.acl.ACLUtils.userAccessHash -import im.actor.server.api.rpc.service.contacts.ContactsServiceImpl -import im.actor.server.api.rpc.service.groups.{ GroupInviteConfig, GroupsServiceImpl } -import im.actor.server.api.rpc.service.pubgroups.PubgroupsServiceImpl -import im.actor.server.api.rpc.service.sequence.{ SequenceServiceConfig, SequenceServiceImpl } -import org.scalatest.Inside._ - -final class PubgroupsServiceSpec - extends BaseAppSuite - with GroupsServiceHelpers - with MessageParsing - with ImplicitSessionRegion - with ImplicitAuthService { - behavior of "PubgroupsService" - - it should "include number of friends in PubGroup" in pendingUntilFixed(t.e1) - - it should "list all public groups with descrition" in pendingUntilFixed(t.e2) - - it should "sort pubgroups by friends count and members count" in pendingUntilFixed(t.e3) - - it should "show number of members and friends to any non-member" in pendingUntilFixed(t.e4) - - val groupInviteConfig = GroupInviteConfig("http://actor.im") - val sequenceConfig = SequenceServiceConfig.load().toOption.get - - val sequenceService = new SequenceServiceImpl(sequenceConfig) - val messagingService = messaging.MessagingServiceImpl() - implicit val groupService = new GroupsServiceImpl(groupInviteConfig) - val pubGroupService = new PubgroupsServiceImpl - val contactService = new ContactsServiceImpl() - - object t { - val (user1, authId1, authSid1, _) = createUser() - val (user2, _, _, _) = createUser() - val (user3, _, _, _) = createUser() - val (user4, _, _, _) = createUser() - val (user5, _, _, _) = createUser() - val (user6, _, _, _) = createUser() - val (user7, _, _, _) = createUser() - val (user8, authId8, authSid8, _) = createUser() - - val sessionId = createSessionId() - implicit val clientData = ClientData(authId1, sessionId, Some(AuthData(user1.id, authSid1, 42))) - - val descriptions = List("Marvelous group for android developers group", "Group for iOS users", "You know it") - - val androidGroup = createPubGroup("Android group", descriptions(0), Set(user2.id, user4.id)).groupPeer - val iosGroup = createPubGroup("iOS group", descriptions(1), Set(user2.id, user3.id, user4.id, user5.id, user6.id, user7.id)).groupPeer - val scalaGroup = createPubGroup("Scala group", descriptions(2), Set(user2.id, user5.id, user6.id, user7.id, user8.id)).groupPeer - val floodGroup = createPubGroup("Flood group", descriptions(2), Set(user2.id, user3.id, user5.id, user6.id, user7.id)).groupPeer - - def e1() = { - whenReady(contactService.handleAddContact(user2.id, userAccessHash(clientData.authId, user2.id, getUserModel(user2.id).accessSalt)))(_ ⇒ ()) - whenReady(pubGroupService.handleGetPublicGroups()) { resp ⇒ - inside(resp) { - case Ok(ResponseGetPublicGroups(groups)) ⇒ - val group = groups.find(_.id == androidGroup.groupId) - group shouldBe defined - group.get.friendsCount shouldEqual 1 - } - } - - whenReady(contactService.handleAddContact(user3.id, userAccessHash(clientData.authId, user3.id, getUserModel(user3.id).accessSalt)))(_ ⇒ ()) //not in group. should not be in friends - whenReady(contactService.handleAddContact(user4.id, userAccessHash(clientData.authId, user4.id, getUserModel(user4.id).accessSalt)))(_ ⇒ ()) - - whenReady(pubGroupService.handleGetPublicGroups()) { resp ⇒ - inside(resp) { - case Ok(ResponseGetPublicGroups(groups)) ⇒ - val group = groups.find(_.id == androidGroup.groupId) - group shouldBe defined - group.get.friendsCount shouldEqual 2 - } - } - } - - def e2() = { - whenReady(pubGroupService.handleGetPublicGroups()) { resp ⇒ - inside(resp) { - case Ok(ResponseGetPublicGroups(groups)) ⇒ - groups should have length 4 - groups.map(_.description).toSet shouldEqual descriptions.toSet - } - } - } - - def e3() = { - /** - * Sorting according number of friends and members - * ios - friends = 3; members = 7 - * flood - friends = 2; members = 6 - * android - friends = 2; members = 3 - * scala - friends = 1; members = 6 - */ - whenReady(pubGroupService.handleGetPublicGroups()) { resp ⇒ - inside(resp) { - case Ok(ResponseGetPublicGroups(groups)) ⇒ - groups.map(_.id) shouldEqual List(iosGroup, floodGroup, androidGroup, scalaGroup).map(_.groupId) - } - } - } - - def e4() = { - implicit val clientData = ClientData(authId8, sessionId, Some(AuthData(user8.id, authSid8, 42))) - whenReady(contactService.handleAddContact(user2.id, userAccessHash(clientData.authId, user2.id, getUserModel(user2.id).accessSalt)))(_ ⇒ ()) - whenReady(contactService.handleAddContact(user3.id, userAccessHash(clientData.authId, user3.id, getUserModel(user3.id).accessSalt)))(_ ⇒ ()) - whenReady(contactService.handleAddContact(user4.id, userAccessHash(clientData.authId, user4.id, getUserModel(user4.id).accessSalt)))(_ ⇒ ()) - - whenReady(pubGroupService.handleGetPublicGroups()) { resp ⇒ - inside(resp) { - case Ok(ResponseGetPublicGroups(groups)) ⇒ - groups.find(_.id == floodGroup.groupId) foreach { g ⇒ - g.friendsCount shouldEqual 2 - g.membersCount shouldEqual 6 - } - //sorting should be the same as in previous example cause we got same contacts - groups.map(_.id) shouldEqual List(iosGroup, floodGroup, androidGroup, scalaGroup).map(_.groupId) - } - } - } - } - -} diff --git a/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/SearchServiceSpec.scala b/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/SearchServiceSpec.scala index e4956cd936..83249eab0b 100644 --- a/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/SearchServiceSpec.scala +++ b/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/SearchServiceSpec.scala @@ -40,9 +40,6 @@ final class SearchServiceSpec groups shouldBe empty users.map(_.id) shouldBe Seq(user2.id) results.length shouldBe 1 - val result = results.head - - result.title shouldBe user2.name } } } @@ -84,4 +81,4 @@ final class SearchServiceSpec } } } -} \ No newline at end of file +} diff --git a/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/SequenceServiceSpec.scala b/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/SequenceServiceSpec.scala index 70166d15e9..b7f3e0ec7a 100644 --- a/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/SequenceServiceSpec.scala +++ b/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/SequenceServiceSpec.scala @@ -4,12 +4,15 @@ import com.typesafe.config.ConfigFactory import im.actor.api.rpc._ import im.actor.api.rpc.contacts.UpdateContactsAdded import im.actor.api.rpc.counters.{ ApiAppCounters, UpdateCountersChanged } -import im.actor.api.rpc.messaging.{ ApiTextMessage, UpdateMessageContentChanged } +import im.actor.api.rpc.messaging.{ ApiTextMessage, UpdateChatGroupsChanged, UpdateMessageContentChanged } import im.actor.api.rpc.misc.ResponseSeq import im.actor.api.rpc.peers.{ ApiPeer, ApiPeerType } import im.actor.api.rpc.sequence.{ ApiUpdateContainer, ApiUpdateOptimization, ResponseGetDifference, UpdateEmptyUpdate } import im.actor.server._ -import im.actor.server.api.rpc.service.sequence.SequenceServiceConfig +import im.actor.server.api.rpc.service.groups.{ GroupInviteConfig, GroupsServiceImpl } +import im.actor.server.api.rpc.service.messaging.MessagingServiceImpl +import im.actor.server.api.rpc.service.sequence.{ SequenceServiceConfig, SequenceServiceImpl } +import im.actor.server.dialog.{ DialogExtension, DialogGroupKeys } import im.actor.server.sequence.{ CommonState, CommonStateVersion, UserSequence } import scala.concurrent.Future @@ -25,6 +28,7 @@ final class SequenceServiceSpec extends BaseAppSuite({ }) with ImplicitSessionRegion with ImplicitAuthService with SeqUpdateMatchers + with GroupsServiceHelpers with ImplicitSequenceService { behavior of "Sequence service" @@ -34,11 +38,14 @@ final class SequenceServiceSpec extends BaseAppSuite({ it should "not produce empty difference if there is one update bigger than difference size limit" in bigUpdate it should "map updates correctly" in mapUpdates it should "exclude optimized updates from sequence" in optimizedUpdates + it should "return single UpdateChatGroupsChanged in difference as if it was applied after all message reads" in chatGroupChangedRead + it should "return single UpdateChatGroupsChanged in difference as if it was aplied after all received messages" in chatGroupChangedReceived private val config = SequenceServiceConfig.load().get - implicit lazy val service = new sequence.SequenceServiceImpl(config) - implicit lazy val msgService = messaging.MessagingServiceImpl() + implicit lazy val service = new SequenceServiceImpl(config) + implicit lazy val msgService = MessagingServiceImpl() + implicit lazy val groupService = new GroupsServiceImpl(GroupInviteConfig("")) def getState() = { val (user, authId, authSid, _) = createUser() @@ -271,4 +278,139 @@ final class SequenceServiceSpec extends BaseAppSuite({ } } } + + def chatGroupChangedRead(): Unit = { + val (alice, aliceAuthId, aliceAuthSid, _) = createUser() + + val sessionId = createSessionId() + val aliceClientData = ClientData(aliceAuthId, sessionId, Some(AuthData(alice.id, aliceAuthSid, 42))) + + val (bob, bobAuthId, bobAuthSid, _) = createUser() + + val group = { + implicit val cd = aliceClientData + createGroup("Some group", Set(bob.id)).groupPeer + } + + val groupReadDates = { + implicit val cd = ClientData(bobAuthId, sessionId, Some(AuthData(bob.id, bobAuthSid, 42))) + 1 to 20 map { i ⇒ + sendMessageToGroup(group.groupId, textMessage(s"Hello in group $i"))._2.date + } + } + + val bobReadDates = { + implicit val cd = ClientData(bobAuthId, sessionId, Some(AuthData(bob.id, bobAuthSid, 42))) + 1 to 20 map { i ⇒ + sendMessageToUser(alice.id, s"Hello $i")._2.date + } + } + + { + implicit val cd = aliceClientData + // FAVOURITE + whenReady(msgService.handleFavouriteDialog(getOutPeer(bob.id, aliceAuthId)))(identity) + + // read 5 messages, 15 left + whenReady(msgService.handleMessageRead(getOutPeer(bob.id, aliceAuthId), bobReadDates(4)))(identity) + + // UNFAVOURITE + whenReady(msgService.handleUnfavouriteDialog(getOutPeer(bob.id, aliceAuthId)))(identity) + + // read 10 messages, 10 left + whenReady(msgService.handleMessageRead(getOutPeer(bob.id, aliceAuthId), bobReadDates(9)))(identity) + + // read all messages in group + whenReady(msgService.handleMessageRead(group.asOutPeer, groupReadDates.last))(identity) + } + + { + implicit val cd = ClientData(bobAuthId, sessionId, Some(AuthData(bob.id, bobAuthSid, 42))) + 1 to 10 map { i ⇒ + sendMessageToUser(alice.id, s"Hello $i")._2.date + } + } + + { + implicit val cd = aliceClientData + + whenReady(service.handleGetDifference(0, Array.empty, Vector.empty)) { res ⇒ + inside(res) { + case Ok(rsp: ResponseGetDifference) ⇒ + val groupedChatsUpdates = rsp.updates.filter(_.updateHeader == UpdateChatGroupsChanged.header) + groupedChatsUpdates should have length 1 + } + } + + expectUpdate(classOf[UpdateChatGroupsChanged]) { upd ⇒ + val optDirect = upd.dialogs.find(_.key == DialogGroupKeys.Direct) + optDirect shouldBe defined + + val bobsDialog = optDirect.get.dialogs.find(_.peer.id == bob.id) + bobsDialog.get.counter shouldEqual 20 + + val optGroups = upd.dialogs.find(_.key == DialogGroupKeys.Groups) + optGroups shouldBe defined + + val groupDialog = optGroups.get.dialogs.find(_.peer.id == group.groupId) + groupDialog.get.counter shouldEqual 0 + } + } + + } + + def chatGroupChangedReceived(): Unit = { + val (alice, aliceAuthId, aliceAuthSid, _) = createUser() + + val sessionId = createSessionId() + val aliceClientData = ClientData(aliceAuthId, sessionId, Some(AuthData(alice.id, aliceAuthSid, 42))) + + val (bob, bobAuthId, bobAuthSid, _) = createUser() + val bobClientData = ClientData(bobAuthId, sessionId, Some(AuthData(bob.id, bobAuthSid, 42))) + + { + implicit val cd = bobClientData + 1 to 10 map { i ⇒ + sendMessageToUser(alice.id, s"Hello $i")._2.date + } + } + + { + implicit val cd = aliceClientData + // FAVOURITE + whenReady(msgService.handleFavouriteDialog(getOutPeer(bob.id, aliceAuthId)))(identity) + + // UNFAVOURITE + whenReady(msgService.handleUnfavouriteDialog(getOutPeer(bob.id, aliceAuthId)))(identity) + } + + { + implicit val cd = bobClientData + 1 to 5 map { i ⇒ + sendMessageToUser(alice.id, s"Hello $i")._2.date + } + } + + { + implicit val cd = aliceClientData + + whenReady(service.handleGetDifference(0, Array.empty, Vector.empty)) { res ⇒ + inside(res) { + case Ok(rsp: ResponseGetDifference) ⇒ + val groupedChatsUpdates = rsp.updates.filter(_.updateHeader == UpdateChatGroupsChanged.header) + groupedChatsUpdates should have length 1 + } + } + + expectUpdate(classOf[UpdateChatGroupsChanged]) { upd ⇒ + val optDirect = upd.dialogs.find(_.key == DialogGroupKeys.Direct) + optDirect shouldBe defined + + val bobsDialog = optDirect.get.dialogs.find(_.peer.id == bob.id) + bobsDialog.get.counter shouldEqual 15 + } + } + + } + } diff --git a/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/auth/AuthServiceSpec.scala b/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/auth/AuthServiceSpec.scala index 957e2abdc9..7f5c1dc89c 100644 --- a/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/auth/AuthServiceSpec.scala +++ b/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/auth/AuthServiceSpec.scala @@ -962,7 +962,7 @@ final class AuthServiceSpec val sessionId = createSessionId() implicit val clientData = ClientData(authId, sessionId, Some(AuthData(user.id, authSid, 42))) - seqUpdExt.registerGooglePushCredentials(model.push.GooglePushCredentials(authId, 22L, "hello")) + seqUpdExt.registerGCMPushCredentials(model.push.GCMPushCredentials(authId, 22L, "hello")) seqUpdExt.registerApplePushCredentials(model.push.ApplePushCredentials(authId, Some(Int32Value(22)), ByteString.copyFrom("hello".getBytes))) //let seqUpdateManager register credentials diff --git a/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/GroupsServiceSpec.scala b/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/groups/v1/GroupsServiceSpec.scala similarity index 97% rename from actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/GroupsServiceSpec.scala rename to actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/groups/v1/GroupsServiceSpec.scala index 84b300db8b..f8ae7eeea3 100644 --- a/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/GroupsServiceSpec.scala +++ b/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/groups/v1/GroupsServiceSpec.scala @@ -1,4 +1,4 @@ -package im.actor.server.api.rpc.service +package im.actor.server.api.rpc.service.groups.v1 import im.actor.api.rpc._ import im.actor.api.rpc.counters.UpdateCountersChanged @@ -68,15 +68,15 @@ final class GroupsServiceSpec "EditGroupAbout" should "allow group admin to change 'about'" in e18 - it should "forbid to change 'about' by non-admin" in e19 + it should "forbid to change 'about' by non-admin" in pendingUntilFixed(e19) it should "set 'about' to empty when None comes" in e20 - it should "forbid to set invalid 'about' field (empty, or longer than 255 characters)" in e21 + it should "forbid to set 'about' field longer than 255 characters" in e21 "EditGroupTopic" should "allow any group member to change topic" in e22 - it should "forbid to set invalid topic (empty, or longer than 255 characters)" in e23 + it should "forbid to set topic longer than 255 characters" in e23 it should "set topic to empty when None comes" in e24 @@ -703,7 +703,7 @@ final class GroupsServiceSpec } whenReady(service.handleMakeUserAdmin(groupOutPeer, user3OutPeer)(clientData2)) { resp ⇒ - resp shouldEqual Error(CommonRpcErrors.forbidden("Only admin can perform this action.")) + resp shouldEqual Error(GroupRpcErrors.NoPermission) } } @@ -761,7 +761,7 @@ final class GroupsServiceSpec } whenReady(service.handleEditGroupAbout(groupOutPeer, 1L, Some("It is group for fun"), Vector.empty)(clientData2)) { resp ⇒ - resp shouldEqual Error(CommonRpcErrors.forbidden("Only admin can perform this action.")) + resp shouldEqual Error(GroupRpcErrors.NoPermission) } } @@ -794,11 +794,6 @@ final class GroupsServiceSpec resp shouldEqual Error(GroupRpcErrors.AboutTooLong) } - val emptyAbout = "" - whenReady(service.handleEditGroupAbout(groupOutPeer, 1L, Some(emptyAbout), Vector.empty)) { resp ⇒ - resp shouldEqual Error(GroupRpcErrors.AboutTooLong) - } - val groupAbout = groupExt.getApiFullStruct(groupOutPeer.groupId, user.id).futureValue.about groupAbout shouldEqual None } @@ -845,11 +840,6 @@ final class GroupsServiceSpec resp shouldEqual Error(GroupRpcErrors.TopicTooLong) } - val emptyTopic = "" - whenReady(service.handleEditGroupTopic(groupOutPeer, 2L, Some(emptyTopic), Vector.empty)) { resp ⇒ - resp shouldEqual Error(GroupRpcErrors.TopicTooLong) - } - val groupTopic = groupExt.getApiFullStruct(groupOutPeer.groupId, user.id).futureValue.theme groupTopic shouldEqual None } diff --git a/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/groups/v2/GroupV2GroupSpec.scala b/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/groups/v2/GroupV2GroupSpec.scala new file mode 100644 index 0000000000..752ed6f3b9 --- /dev/null +++ b/actor-server/actor-tests/src/test/scala/im/actor/server/api/rpc/service/groups/v2/GroupV2GroupSpec.scala @@ -0,0 +1,170 @@ +package im.actor.server.api.rpc.service.groups.v2 + +import cats.data.Xor +import im.actor.api.rpc.groups.{ ApiGroupType, UpdateGroupInviteObsolete } +import im.actor.api.rpc.messaging.UpdateMessage +import im.actor.api.rpc.peers.{ ApiGroupOutPeer, ApiUserOutPeer } +import im.actor.api.rpc.{ AuthData, ClientData, Ok, PeersImplicits, RpcError, RpcResponse } +import im.actor.api.rpc.sequence.ApiUpdateOptimization +import im.actor.server._ +import im.actor.server.api.rpc.service.groups.{ GroupInviteConfig, GroupsServiceImpl } +import im.actor.server.group.{ GroupExtension, GroupServiceMessages, GroupType } +import im.actor.server.sequence.SeqState +import im.actor.server.user.UserExtension +import im.actor.util.ThreadLocalSecureRandom + +import scala.concurrent.Future + +class GroupV2GroupSpec extends BaseAppSuite + // with GroupsServiceHelpers + with MessageParsing + // with MessagingSpecHelpers + with ImplicitSequenceService + with ImplicitAuthService + with ImplicitMessagingService + with ImplicitSessionRegion + with SeqUpdateMatchers + with PeersImplicits { + + behavior of "Groups V2 API" + + it should "create group and return correct members list" in create + + it should "allow ONLY group members to invite other users" in invite + + // it should "allow only group admin to change about" in changeAbout + + // it should "allow only group admin to revoke integration token" in revokeToken + + // join + // invite + // kick + // leave + // change actions by admin + // change actions by non-admin + + val optimizations = Vector(ApiUpdateOptimization.GROUPS_V2, ApiUpdateOptimization.STRIP_ENTITIES) + + val groupInviteConfig = GroupInviteConfig("http://actor.im") + val groupExt = GroupExtension(system) + + val groupService = new GroupsServiceImpl(groupInviteConfig) + + val userExt = UserExtension(system) + + def create(): Unit = { + val (alice, aliceAuthId, aliceAuthSid, _) = createUser() + val sessionId = createSessionId() + + implicit val clientData = ClientData(aliceAuthId, sessionId, Some(AuthData(alice.id, aliceAuthSid, 42))) + val aliceOutPeer = getUserOutPeer(alice.id, aliceAuthId) + + val members = 1 to 20 map { i ⇒ + val (user, authId, authSid, _) = createUser() + getUserOutPeer(user.id, aliceAuthId) + } + + val title = "V2 first group" + val randomId = nextRandomId() + val createResp = response(groupService.handleCreateGroup( + randomId, + title = title, + users = members, + groupType = None, + optimizations = optimizations + )) + + // members checks + createResp.users shouldBe empty // strip entities optimization + createResp.userPeers should not be empty + createResp.userPeers should have length 21 // 20 invited users + 1 creator + createResp.group.membersCount shouldEqual Some(21) + createResp.userPeers should contain theSameElementsAs (members :+ aliceOutPeer) + + // group properties checks + createResp.group.groupType shouldEqual Some(ApiGroupType.GROUP) + createResp.group.title shouldEqual title + createResp.group.isHidden shouldEqual Some(false) + + // membership checks + createResp.group.isMember shouldEqual Some(true) + createResp.group.isAdmin shouldEqual Some(true) + createResp.group.creatorUserId shouldEqual alice.id + + // Group V2 updates + expectUpdate(classOf[UpdateMessage]) { upd ⇒ + upd.randomId shouldEqual randomId + upd.message shouldEqual GroupServiceMessages.groupCreated + } + + // There should be no Group V1 updates + expectNoUpdate(emptyState, classOf[UpdateGroupInviteObsolete]) + } + + def invite() = { + val (alice, aliceAuthId, aliceAuthSid, _) = createUser() + val (bob, bobAuthId, bobAuthSid, _) = createUser() + val (carol, _, _, _) = createUser() + + val sessionId = createSessionId() + + val aliceClientData = ClientData(aliceAuthId, sessionId, Some(AuthData(alice.id, aliceAuthSid, 42))) + val bobClientData = ClientData(bobAuthId, sessionId, Some(AuthData(bob.id, bobAuthSid, 42))) + + val randomId = nextRandomId() + val (seqState, groupOutPeer) = { + implicit val cd = aliceClientData + val createResp = response(groupService.handleCreateGroup( + randomId, + title = "Invite check group", + users = Vector.empty, + groupType = None, + optimizations = optimizations + )) + val group = createResp.group + group.membersCount shouldEqual Some(1) + group.members should have length 1 + group.members map (_.userId) shouldEqual Vector(alice.id) + + mkSeqState(createResp.seq, createResp.state) → ApiGroupOutPeer(group.id, group.accessHash) + } + + // Don't allow non-member to invite other users + { + implicit val cd = bobClientData + whenReady(groupService.handleInviteUser( + groupOutPeer, + nextRandomId(), + getUserOutPeer(carol.id, bobAuthId), + optimizations + )) { _ should matchForbidden } + } + + // Allow group member to invite other users + { + implicit val cd = aliceClientData + val randomId = nextRandomId() + whenReady(groupService.handleInviteUser( + groupOutPeer, + randomId, + getUserOutPeer(bob.id, aliceAuthId), + optimizations + )) { resp ⇒ + + } + + } + + } + + private def response[A <: RpcResponse](result: Future[RpcError Xor A]): A = { + val r = result.futureValue + r should matchPattern { + case Ok(_) ⇒ + } + r.getOrElse(fail("Rpc response was not OK")) + } + + private def nextRandomId() = ThreadLocalSecureRandom.current().nextLong() + +} diff --git a/actor-server/actor-tests/src/test/scala/im/actor/server/enrich/PreviewMakerSpec.scala b/actor-server/actor-tests/src/test/scala/im/actor/server/enrich/PreviewMakerSpec.scala index aa83369c55..f0aadbbcd1 100644 --- a/actor-server/actor-tests/src/test/scala/im/actor/server/enrich/PreviewMakerSpec.scala +++ b/actor-server/actor-tests/src/test/scala/im/actor/server/enrich/PreviewMakerSpec.scala @@ -35,7 +35,7 @@ class PreviewMakerSpec extends BaseRichMessageSpec { implicit val probe = TestProbe() val config = RichMessageConfig(5 * 1024 * 1024) - val previewMaker = PreviewMaker(config, "previewMaker" + new DateTime) + val previewMaker = system.actorOf(PreviewMaker.props(config)) import PreviewMaker._ @@ -88,7 +88,7 @@ class PreviewMakerSpec extends BaseRichMessageSpec { def imageTooLarge() = { val image = Images.withNameHttp val config = RichMessageConfig(image.contentLength - 1000L) - val previewMaker = PreviewMaker(config, "previewMaker" + new DateTime) + val previewMaker = system.actorOf(PreviewMaker.props(config)) sendGetPreview(previewMaker, image.url) probe watch previewMaker diff --git a/actor-server/actor-tests/src/test/scala/im/actor/server/user/ContactRegisteredSpec.scala b/actor-server/actor-tests/src/test/scala/im/actor/server/user/ContactRegisteredSpec.scala index c84b3b4438..ba40d6086d 100644 --- a/actor-server/actor-tests/src/test/scala/im/actor/server/user/ContactRegisteredSpec.scala +++ b/actor-server/actor-tests/src/test/scala/im/actor/server/user/ContactRegisteredSpec.scala @@ -87,7 +87,7 @@ final class ContactRegisteredSpec extends BaseAppSuite with ImplicitAuthService val (alice, aliceAuthId, aliceAuthSid, _) = createUser() val aliceClientData = ClientData(aliceAuthId, 1, Some(AuthData(alice.id, aliceAuthSid, 42))) - whenReady(db.run(UnregisteredEmailContactRepo.create("test@acme.com", alice.id, None)))(identity) + whenReady(db.run(UnregisteredEmailContactRepo.createIfNotExists("test@acme.com", alice.id, None)))(identity) val (bob, bobAuthId, bobAuthSid, _) = createUser() val bobClientData = ClientData(bobAuthId, 1, Some(AuthData(bob.id, bobAuthSid, 42))) @@ -96,4 +96,4 @@ final class ContactRegisteredSpec extends BaseAppSuite with ImplicitAuthService (aliceClientData, bobClientData) } -} \ No newline at end of file +} diff --git a/actor-server/actor-tests/src/test/scala/im/actor/util/StringUtilsSpec.scala b/actor-server/actor-tests/src/test/scala/im/actor/util/StringUtilsSpec.scala index 7b25b68cf5..945e7a17cd 100644 --- a/actor-server/actor-tests/src/test/scala/im/actor/util/StringUtilsSpec.scala +++ b/actor-server/actor-tests/src/test/scala/im/actor/util/StringUtilsSpec.scala @@ -1,6 +1,6 @@ package im.actor.util -import im.actor.util.misc.StringUtils.{ transliterate, validUsername } +import im.actor.util.misc.StringUtils.{ transliterate, validGlobalName } import org.scalatest.{ Matchers, FlatSpecLike } class StringUtilsSpec extends FlatSpecLike with Matchers { @@ -10,19 +10,19 @@ class StringUtilsSpec extends FlatSpecLike with Matchers { "transliterate" should "transform string to lower-cased string with only latin chars" in translit def nicknames() = { - validUsername("rockjam") shouldEqual true - validUsername("abcde") shouldEqual true - validUsername("rock_jam") shouldEqual true - validUsername("r0ck_jaM___") shouldEqual true + validGlobalName("rockjam") shouldEqual true + validGlobalName("abcde") shouldEqual true + validGlobalName("rock_jam") shouldEqual true + validGlobalName("r0ck_jaM___") shouldEqual true //too long val tooLong = 0 to 35 map (e ⇒ ".") mkString "" - validUsername(tooLong) shouldEqual false + validGlobalName(tooLong) shouldEqual false //too short - validUsername("roc") shouldEqual false + validGlobalName("roc") shouldEqual false //wrong symbols - validUsername("rock-jam") shouldEqual false - validUsername("rock&^^jam") shouldEqual false + validGlobalName("rock-jam") shouldEqual false + validGlobalName("rock&^^jam") shouldEqual false } def translit() = { diff --git a/actor-server/build.sbt b/actor-server/build.sbt index 0f290c6065..8f24fd276c 100644 --- a/actor-server/build.sbt +++ b/actor-server/build.sbt @@ -2,3 +2,5 @@ addCommandAlias("debianPackage", "debian:packageBin") addCommandAlias("debianPackageSystemd", "; set serverLoading in Debian := com.typesafe.sbt.packager.archetypes.ServerLoader.Systemd ;debian:packageBin" ) + +defaultLinuxInstallLocation in Docker := "/var/lib/actor" diff --git a/actor-server/docker.sh b/actor-server/docker.sh new file mode 100755 index 0000000000..bd8e3c6edf --- /dev/null +++ b/actor-server/docker.sh @@ -0,0 +1,3 @@ +#! /bin/bash + +./sbt docker:stage && docker build --no-cache=true -f Dockerfile -t actor/server . diff --git a/actor-server/notes/3.0.0.markdown b/actor-server/notes/3.0.0.markdown new file mode 100644 index 0000000000..c4d82de026 --- /dev/null +++ b/actor-server/notes/3.0.0.markdown @@ -0,0 +1,14 @@ +### Release notes +* :shipit: a lot of new group features +* :bug: various bugfixes +* :rocket: server dependencies updates + +#### New groups :tada: +This release focused on group features, including: +* private and public channels; +* short names for public groups. Just like user can have nickname, group can have short names assigned by owner. Short name can be used to find group in global search, or invite people with nice invite url. +* shared history for public groups; +* multiple group admins; +* ownership transfer; +* fine grained permission settings for admins/members. Owner of group decides what members can/can't do; +* optimized performance for big groups. diff --git a/actor-server/project/Build.scala b/actor-server/project/Build.scala index f35f67fcb9..7d27bc16d5 100644 --- a/actor-server/project/Build.scala +++ b/actor-server/project/Build.scala @@ -23,7 +23,13 @@ object Build extends sbt.Build with Versioning with Releasing with Packaging { organization := "im.actor.server", organizationHomepage := Some(url("https://actor.im")), resolvers ++= Resolvers.seq, - scalacOptions ++= Seq("-Yopt-warnings"), +// scalacOptions ++= Seq( +// "-Ywarn-unused", +// "-Ywarn-adapted-args", +// "-Ywarn-nullary-override", +// "-Ywarn-nullary-unit", +// "-Ywarn-value-discard" +// ), parallelExecution := true ) ++ Sonatype.sonatypeSettings @@ -82,7 +88,8 @@ object Build extends sbt.Build with Versioning with Releasing with Packaging { }, resolvers ++= Resolvers.seq, fork in Test := false, - updateOptions := updateOptions.value.withCachedResolution(true) + updateOptions := updateOptions.value.withCachedResolution(true), + addCompilerPlugin("com.github.ghik" % "silencer-plugin" % "0.4") ) lazy val root = Project( @@ -174,8 +181,7 @@ object Build extends sbt.Build with Versioning with Releasing with Packaging { id = "actor-core", base = file("actor-core"), settings = defaultSettingsServer ++ SbtActorApi.settings ++ Seq( - libraryDependencies ++= Dependencies.core, - scalacOptions in Compile := (scalacOptions in Compile).value.filterNot(_ == "-Xfatal-warnings") + libraryDependencies ++= Dependencies.core ) ) .dependsOn(actorCodecs, actorFileAdapter, actorModels, actorPersist, actorRuntime) @@ -194,8 +200,7 @@ object Build extends sbt.Build with Versioning with Releasing with Packaging { id = "actor-enrich", base = file("actor-enrich"), settings = defaultSettingsServer ++ Seq( - libraryDependencies ++= Dependencies.enrich, - scalacOptions in Compile := (scalacOptions in Compile).value.filterNot(_ == "-Xfatal-warnings") + libraryDependencies ++= Dependencies.enrich ) ) .dependsOn(actorRpcApi, actorRuntime) @@ -234,7 +239,7 @@ object Build extends sbt.Build with Versioning with Releasing with Packaging { libraryDependencies ++= Dependencies.session ) ) - .dependsOn(actorPersist, actorCore, actorCodecs, actorCore, actorRpcApi) + .dependsOn(actorCodecs, actorCore, actorPersist, actorRpcApi) lazy val actorSessionMessages = Project( id = "actor-session-messages", @@ -247,8 +252,7 @@ object Build extends sbt.Build with Versioning with Releasing with Packaging { id = "actor-rpc-api", base = file("actor-rpc-api"), settings = defaultSettingsServer ++ Seq( - libraryDependencies ++= Dependencies.rpcApi, - scalacOptions in Compile := (scalacOptions in Compile).value.filterNot(_ == "-Xfatal-warnings") + libraryDependencies ++= Dependencies.rpcApi ) ) .dependsOn( @@ -269,8 +273,7 @@ object Build extends sbt.Build with Versioning with Releasing with Packaging { id = "actor-fs-adapters", base = file("actor-fs-adapters"), settings = defaultSettingsServer ++ Seq( - libraryDependencies ++= Dependencies.fileAdapter, - scalacOptions in Compile := (scalacOptions in Compile).value.filterNot(_ == "-Xfatal-warnings") + libraryDependencies ++= Dependencies.fileAdapter ) ) .dependsOn(actorHttpApi, actorPersist) @@ -279,8 +282,7 @@ object Build extends sbt.Build with Versioning with Releasing with Packaging { id = "actor-frontend", base = file("actor-frontend"), settings = defaultSettingsServer ++ Seq( - libraryDependencies ++= Dependencies.frontend, - scalacOptions in Compile := (scalacOptions in Compile).value.filterNot(_ == "-Xfatal-warnings") + libraryDependencies ++= Dependencies.frontend ) ) .dependsOn(actorCore, actorSession) diff --git a/actor-server/project/Dependencies.scala b/actor-server/project/Dependencies.scala index 26cb76681b..d237fc0ae0 100644 --- a/actor-server/project/Dependencies.scala +++ b/actor-server/project/Dependencies.scala @@ -4,15 +4,15 @@ import sbt._ object Dependencies { object V { - val actorCommons = "0.0.18" - val actorBotkit = "1.0.109" - val akka = "2.4.7" - val akkaHttpJson = "1.5.0" - val cats = "0.3.0" - val circe = "0.2.1" + val actorCommons = "0.0.20" + val actorBotkit = "1.0.113" + val akka = "2.4.10" + val akkaHttpJson = "1.10.0" + val cats = "0.7.2" + val circe = "0.5.1" val kamon = "0.5.2" val slick = "3.1.1" - val slickPg = "0.13.0" + val slickPg = "0.14.3" val scalatest = "2.2.4" val shardakka = "0.1.24" val scalapbSer = "0.1.14" @@ -48,18 +48,18 @@ object Dependencies { val caffeine = "com.github.ben-manes.caffeine" % "caffeine" % "2.2.7" - val cats = "org.spire-math" %% "cats" % V.cats + val cats = "org.typelevel" %% "cats" % V.cats val circeCore = "io.circe" %% "circe-core" % V.circe val circeGeneric = "io.circe" %% "circe-generic" % V.circe - val circeParse = "io.circe" %% "circe-parse" % V.circe + val circeParse = "io.circe" %% "circe-parser" % V.circe val configs = "com.github.kxbmap" %% "configs" % "0.3.0" val dispatch = "net.databinder.dispatch" %% "dispatch-core" % "0.11.3" val javaCompat = "org.scala-lang.modules" %% "scala-java8-compat" % "0.7.0" - val playJson = "com.typesafe.play" %% "play-json" % "2.4.2" + val playJson = "com.typesafe.play" %% "play-json" % "2.5.6" val upickle = "com.lihaoyi" %% "upickle" % "0.3.6" val postgresJdbc = "org.postgresql" % "postgresql" % "9.4.1208" exclude("org.slf4j", "slf4j-simple") @@ -72,8 +72,8 @@ object Dependencies { val flywayCore = "org.flywaydb" % "flyway-core" % "3.1" val hikariCP = "com.zaxxer" % "HikariCP" % "2.4.6" - val amazonaws = "com.amazonaws" % "aws-java-sdk-s3" % "1.9.31" - val awsWrap = "com.github.dwhjames" %% "aws-wrap" % "0.7.2" + val amazonaws = "com.amazonaws" % "aws-java-sdk-s3" % "1.11.32" + val awsWrap = "com.github.dwhjames" %% "aws-wrap" % "0.8.0" val bcprov = "org.bouncycastle" % "bcprov-jdk15on" % "1.50" @@ -109,6 +109,7 @@ object Dependencies { val guava = "com.google.guava" % "guava" % "19.0" val alpn = "org.eclipse.jetty.alpn" % "alpn-api" % "1.1.2.v20150522" % "runtime" val tcnative = "io.netty" % "netty-tcnative" % "1.1.33.Fork15" classifier "linux-x86_64" + val silencer = "com.github.ghik" % "silencer-lib" % "0.4" } object Testing { @@ -134,7 +135,8 @@ object Dependencies { scalaLogging, tyrex, kamonCore, - kamonDatadog + kamonDatadog, + silencer ) val root = shared ++ Seq( @@ -206,7 +208,5 @@ object Dependencies { val runtime = shared ++ Seq(akkaActor, actorConcurrent, akkaHttp, akkaSlf4j, akkaStream, akkaPersistenceJdbc, apacheCommonsCodec, caffeine, cats, jodaConvert, jodaTime, icu4j, libPhoneNumber, scalapbSer, akkaTestkit % "test", scalatest % "test") - val voximplant = shared ++ Seq(akkaActor, dispatch, playJson) - val tests = shared ++ Seq(akkaClusterSharding, amazonaws, jfairy, scalacheck, scalatest, slickTestkit, akkaTestkit, akkaMultiNodeTestkit) } diff --git a/actor-server/project/build.properties b/actor-server/project/build.properties index 43b8278c68..35c88bab7d 100644 --- a/actor-server/project/build.properties +++ b/actor-server/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.11 +sbt.version=0.13.12 diff --git a/actor-server/src/docker/opt/docker/conf/server.conf b/actor-server/src/docker/opt/docker/conf/server.conf deleted file mode 100644 index 0a45894e55..0000000000 --- a/actor-server/src/docker/opt/docker/conf/server.conf +++ /dev/null @@ -1,52 +0,0 @@ -// We need it in application.conf because reference.conf can't refer to application.conf, this is a work-around - -modules { - files { - adapter: "im.actor.server.file.local.LocalFileStorageAdapter" - } -} - -services { - postgresql { - host: "postgres" - host: ${?DB_HOST} - - db: postgres - db: ${?DB_NAME} - - user: "postgres" - user: ${?DB_USER} - - password: "" - password: ${?DB_PASSWORD} - } - - actor-activation { - uri: "https://activation-gw.actor.im" - auth-token: "FPEinjrmxsq1ZDyu1bc7" - auth-token: ${?ACTIVATION_GW_TOKEN} - } - - file-storage { - location: "/var/lib/actor/files" - location: ${?FILESTORAGE_LOCATION} - } -} - -http { - static-files-directory: "/opt/docker/files" -} - -secret: ${?SECRET} - -akka { - log-config-on-start: true - - cluster { - seed-nodes = ["akka.tcp://actor-server@127.0.0.1:2552"] - } - - remote { - netty.tcp.hostname = "127.0.0.1" - } -} diff --git a/actor-server/src/docker/var/lib/actor/conf/custom.conf b/actor-server/src/docker/var/lib/actor/conf/custom.conf new file mode 100644 index 0000000000..cee6a29e8e --- /dev/null +++ b/actor-server/src/docker/var/lib/actor/conf/custom.conf @@ -0,0 +1 @@ +# file to include custom configuration, alongside with server.conf diff --git a/actor-server/src/docker/opt/docker/conf/logback.xml b/actor-server/src/docker/var/lib/actor/conf/logback.xml similarity index 87% rename from actor-server/src/docker/opt/docker/conf/logback.xml rename to actor-server/src/docker/var/lib/actor/conf/logback.xml index 762dabf245..a97393d26d 100644 --- a/actor-server/src/docker/opt/docker/conf/logback.xml +++ b/actor-server/src/docker/var/lib/actor/conf/logback.xml @@ -3,7 +3,6 @@ - @@ -11,7 +10,6 @@ - true @@ -22,7 +20,7 @@ - + diff --git a/actor-server/src/docker/var/lib/actor/conf/server.conf b/actor-server/src/docker/var/lib/actor/conf/server.conf new file mode 100644 index 0000000000..6f62ce904d --- /dev/null +++ b/actor-server/src/docker/var/lib/actor/conf/server.conf @@ -0,0 +1,116 @@ +include file("conf/custom.conf") + +secret: ${?ACTOR_SECRET} + +services { + postgresql { + host: "postgres" + host: ${?ACTOR_DB_HOST} + + db: actor + db: ${?ACTOR_DB_NAME} + + user: "actor" + user: ${?ACTOR_DB_USER} + + password: "" + password: ${?ACTOR_DB_PASSWORD} + } + + actor-activation { + uri: "https://gate.actor.im" + auth-token: ${?ACTOR_GATE_TOKEN} + } + + actor { + push { + token: "" + token: ${?ACTOR_PUSH_TOKEN} + } + } + + file-storage { + location: "/files" + location: ${?ACTOR_FILESTORAGE_LOCATION} + } + + email { + sender { + address: "" + address: ${?ACTOR_EMAIL_SENDER_ADDRESS} + name: "Actor" + name: ${?ACTOR_EMAIL_SENDER_NAME} + prefix: "[Actor]" + } + + smtp { + host: "" + host: ${?ACTOR_SMTP_HOST} + port: 465 + port: ${?ACTOR_SMTP_PORT} + username: "" + username: ${?ACTOR_SMTP_USERNAME} + password: "" + password: ${?ACTOR_SMTP_PASSWORD} + tls: true + password: ${?ACTOR_SMTP_TLS} + } + } + +} + +http { + base-uri = ${?ACTOR_API_ENDPOINT} +} + +modules { + messaging { + groups { + invite { + base-uri: "https://example.com" + base-uri: ${?ACTOR_INVITE_LINK} + } + } + } + files { + adapter: "im.actor.server.file.local.LocalFileStorageAdapter" + } + security { + server-keys: [ + { + public: "/keys/actor-key-0.pub" + private: "/keys/actor-key-0.key" + } + { + public: "/keys/actor-key-1.pub" + private: "/keys/actor-key-1.key" + } + { + public: "/keys/actor-key-2.pub" + private: "/keys/actor-key-2.key" + } + { + public: "/keys/actor-key-3.pub" + private: "/keys/actor-key-3.key" + } + ] + } + api { + endpoint = ${?ACTOR_API_ENDPOINT} + } +} + +akka { + log-config-on-start: true + loggers: ["akka.event.slf4j.Slf4jLogger"] + loglevel: "DEBUG" + + cluster { + seed-nodes = ["akka.tcp://actor-server@127.0.0.1:2552"] + } + + remote { + netty.tcp.hostname = "127.0.0.1" + } +} + diff --git a/actor-server/src/docker/var/lib/actor/files/init b/actor-server/src/docker/var/lib/actor/files/init deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/actor-server/src/universal/conf/server.conf.example b/actor-server/src/linux/usr/share/actor/conf/server.conf.example similarity index 92% rename from actor-server/src/universal/conf/server.conf.example rename to actor-server/src/linux/usr/share/actor/conf/server.conf.example index 89f82d437d..7e6d0543ef 100644 --- a/actor-server/src/universal/conf/server.conf.example +++ b/actor-server/src/linux/usr/share/actor/conf/server.conf.example @@ -97,22 +97,24 @@ services { # AWS configuration # It is strictly recommended to configure s3 storage for # enabling file sharing in apps - aws { + # aws { # S3 Storage, used for file sharing # For more information see https://github.com/actorapp/actor-bootstrap/blob/master/docs/server/configure-s3.md - s3 { + # s3 { + # Bucket name + # default-bucket: # S3 Api Key - access-key: + # access-key: # S3 Api Secret - secret-key: - # File bucket - default-bucket: + # secret-key: # S3 Endpoint # endpoint: + # S3 bucket region + # region: # Enable S3 URLs with path-style access # path-style-access: true / false - } - } + # } + # } # Service used for sending activation codes activation { @@ -217,8 +219,17 @@ services { # scope: "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.google.com/m8/feeds/" #} - # Android & Chrome push notifications - # push { + # Android push notifications via GCM and Firebase + # gcm { + # # Keys for push notifications in format + # keys = [ + # { + # project-id: + # key: + # } + # ] + # } + # firebase { # # Keys for push notifications in format # keys = [ # { diff --git a/actor-server/src/universal/conf/server.conf.example-minimal b/actor-server/src/linux/usr/share/actor/conf/server.conf.example-minimal similarity index 100% rename from actor-server/src/universal/conf/server.conf.example-minimal rename to actor-server/src/linux/usr/share/actor/conf/server.conf.example-minimal diff --git a/actor-server/src/main/scala/im/actor/server/Main.scala b/actor-server/src/main/scala/im/actor/server/Main.scala index 261b5dab2d..a5a139a0f1 100644 --- a/actor-server/src/main/scala/im/actor/server/Main.scala +++ b/actor-server/src/main/scala/im/actor/server/Main.scala @@ -4,5 +4,7 @@ import scala.concurrent.Await import scala.concurrent.duration.Duration object Main extends App { - Await.result(ActorServer.newBuilder.start().system.whenTerminated, Duration.Inf) -} \ No newline at end of file + val system = ActorServer.newBuilder.start().system + sys.addShutdownHook(system.terminate()) + Await.result(system.whenTerminated, Duration.Inf) +} diff --git a/actor-server/version.sbt b/actor-server/version.sbt index e23b1fae2e..d46b3d6f4b 100644 --- a/actor-server/version.sbt +++ b/actor-server/version.sbt @@ -1 +1 @@ -version in ThisBuild := "1.0.154-SNAPSHOT" +version in ThisBuild := "3.0.1-SNAPSHOT" \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1d10ddc013..b504ce79f6 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Apr 09 16:04:30 MSK 2016 +#Wed Aug 24 15:48:51 MSK 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/settings.gradle b/settings.gradle index 62051e33b9..bdb45b2558 100644 --- a/settings.gradle +++ b/settings.gradle @@ -34,6 +34,12 @@ include ":actor-sdk:sdk-core-android:android-google-maps" include ":actor-sdk:sdk-core-ios" +// +// SDK KeyGen +// + +include ":actor-keygen" + // // SDK Command Line Client //