Message timestamps
This commit is contained in:
parent
6a1d8b61f4
commit
8f33a640ce
@ -12,7 +12,7 @@ object AIRADatabase {
|
||||
external fun setContactSeen(contactUuid: String, seen: Boolean): Boolean
|
||||
external fun changeContactName(contactUuid: String, newName: String): Boolean
|
||||
external fun setContactAvatar(contactUuid: String, avatarUuid: String?): Boolean
|
||||
external fun storeMsg(contactUuid: String, outgoing: Boolean, data: ByteArray): Boolean
|
||||
external fun storeMsg(contactUuid: String, outgoing: Boolean, timestamp: Long, data: ByteArray): Boolean
|
||||
external fun storeFile(contactUuid: String?, data: ByteArray): ByteArray?
|
||||
external fun loadMsgs(uuid: String, offset: Int, count: Int): ArrayList<ChatItem>?
|
||||
external fun loadFile(rawUuid: ByteArray): ByteArray?
|
||||
|
@ -23,6 +23,7 @@ import sushi.hardcore.aira.databinding.DialogFingerprintsBinding
|
||||
import sushi.hardcore.aira.databinding.DialogInfoBinding
|
||||
import sushi.hardcore.aira.utils.FileUtils
|
||||
import sushi.hardcore.aira.utils.StringUtils
|
||||
import sushi.hardcore.aira.utils.TimeUtils
|
||||
|
||||
class ChatActivity : ServiceBoundActivity() {
|
||||
private external fun generateFingerprint(publicKey: ByteArray): String
|
||||
@ -77,7 +78,7 @@ class ChatActivity : ServiceBoundActivity() {
|
||||
val msg = binding.editMessage.text.toString()
|
||||
airaService.sendTo(sessionId, Protocol.newMessage(msg))
|
||||
binding.editMessage.text.clear()
|
||||
chatAdapter.newMessage(ChatItem(true, Protocol.newMessage(msg)))
|
||||
chatAdapter.newMessage(ChatItem(true, TimeUtils.getTimestamp(), Protocol.newMessage(msg)))
|
||||
if (airaService.contacts.contains(sessionId)) {
|
||||
lastLoadedMessageOffset += 1
|
||||
}
|
||||
@ -178,10 +179,10 @@ class ChatActivity : ServiceBoundActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onNewMessage(sessionId: Int, data: ByteArray): Boolean {
|
||||
override fun onNewMessage(sessionId: Int, timestamp: Long, data: ByteArray): Boolean {
|
||||
return if (this@ChatActivity.sessionId == sessionId) {
|
||||
runOnUiThread {
|
||||
chatAdapter.newMessage(ChatItem(false, data))
|
||||
chatAdapter.newMessage(ChatItem(false, timestamp, data))
|
||||
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
|
||||
}
|
||||
if (airaService.contacts.contains(sessionId)) {
|
||||
|
@ -2,7 +2,7 @@ package sushi.hardcore.aira
|
||||
|
||||
import sushi.hardcore.aira.background_service.Protocol
|
||||
|
||||
class ChatItem(val outgoing: Boolean, val data: ByteArray) {
|
||||
class ChatItem(val outgoing: Boolean, val timestamp: Long, val data: ByteArray) {
|
||||
companion object {
|
||||
const val OUTGOING_MESSAGE = 0
|
||||
const val INCOMING_MESSAGE = 1
|
||||
|
@ -73,7 +73,7 @@ class MainActivity : ServiceBoundActivity() {
|
||||
onlineSessionAdapter.setAvatar(sessionId, avatar)
|
||||
}
|
||||
}
|
||||
override fun onNewMessage(sessionId: Int, data: ByteArray): Boolean {
|
||||
override fun onNewMessage(sessionId: Int, timestamp: Long, data: ByteArray): Boolean {
|
||||
runOnUiThread {
|
||||
onlineSessionAdapter.setSeen(sessionId, false)
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
|
@ -1,5 +1,6 @@
|
||||
package sushi.hardcore.aira.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
@ -15,6 +16,8 @@ import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sushi.hardcore.aira.ChatItem
|
||||
import sushi.hardcore.aira.R
|
||||
import sushi.hardcore.aira.utils.StringUtils
|
||||
import java.util.*
|
||||
|
||||
class ChatAdapter(
|
||||
private val context: Context,
|
||||
@ -52,8 +55,9 @@ class ChatAdapter(
|
||||
itemView.updatePadding(left = BUBBLE_HORIZONTAL_PADDING)
|
||||
}
|
||||
}
|
||||
protected fun configureBubble(context: Context, view: View, outgoing: Boolean) {
|
||||
view.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
||||
protected fun configureBubble(context: Context, outgoing: Boolean) {
|
||||
val bubble = itemView.findViewById<LinearLayout>(R.id.bubble_content)
|
||||
bubble.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
||||
gravity = if (outgoing) {
|
||||
marginStart = CONTAINER_MARGIN
|
||||
Gravity.END
|
||||
@ -63,12 +67,21 @@ class ChatAdapter(
|
||||
}
|
||||
}
|
||||
if (!outgoing) {
|
||||
view.background.colorFilter = PorterDuffColorFilter(
|
||||
bubble.background.colorFilter = PorterDuffColorFilter(
|
||||
ContextCompat.getColor(context, R.color.incomingBubbleBackground),
|
||||
PorterDuff.Mode.SRC
|
||||
)
|
||||
}
|
||||
}
|
||||
protected fun setTimestamp(chatItem: ChatItem): TextView {
|
||||
val calendar = Calendar.getInstance().apply {
|
||||
time = Date(chatItem.timestamp * 1000)
|
||||
}
|
||||
val textView = itemView.findViewById<TextView>(R.id.timestamp)
|
||||
@SuppressLint("SetTextI18n")
|
||||
textView.text = StringUtils.toTwoDigits(calendar.get(Calendar.HOUR_OF_DAY))+":"+StringUtils.toTwoDigits(calendar.get(Calendar.MINUTE))
|
||||
return textView
|
||||
}
|
||||
}
|
||||
|
||||
internal open class MessageViewHolder(itemView: View): BubbleViewHolder(itemView) {
|
||||
@ -82,43 +95,58 @@ class ChatAdapter(
|
||||
|
||||
internal class OutgoingMessageViewHolder(private val context: Context, itemView: View): MessageViewHolder(itemView) {
|
||||
fun bind(chatItem: ChatItem) {
|
||||
configureBubble(context, bindMessage(chatItem).apply {
|
||||
setLinkTextColor(ContextCompat.getColor(context, R.color.outgoingTextLink))
|
||||
}, true)
|
||||
setTimestamp(chatItem).apply {
|
||||
setTextColor(ContextCompat.getColor(context, R.color.outgoingTimestamp))
|
||||
}
|
||||
configureBubble(context, true)
|
||||
bindMessage(chatItem).apply {
|
||||
setLinkTextColor(ContextCompat.getColor(context, R.color.outgoingTextLink))
|
||||
}
|
||||
setPadding(true)
|
||||
}
|
||||
}
|
||||
|
||||
internal class IncomingMessageViewHolder(private val context: Context, itemView: View): MessageViewHolder(itemView) {
|
||||
fun bind(chatItem: ChatItem) {
|
||||
configureBubble(context, bindMessage(chatItem).apply {
|
||||
setTimestamp(chatItem).apply {
|
||||
setTextColor(ContextCompat.getColor(context, R.color.incomingTimestamp))
|
||||
}
|
||||
configureBubble(context, false)
|
||||
bindMessage(chatItem).apply {
|
||||
setLinkTextColor(ContextCompat.getColor(context, R.color.incomingTextLink))
|
||||
}, false)
|
||||
}
|
||||
setPadding(false)
|
||||
}
|
||||
}
|
||||
|
||||
internal open class FileViewHolder(itemView: View, private val onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): BubbleViewHolder(itemView) {
|
||||
protected fun bindFile(chatItem: ChatItem): LinearLayout {
|
||||
protected fun bindFile(chatItem: ChatItem) {
|
||||
val filename = chatItem.data.sliceArray(17 until chatItem.data.size).decodeToString()
|
||||
itemView.findViewById<TextView>(R.id.text_filename).text = filename
|
||||
itemView.findViewById<ImageButton>(R.id.button_save).setOnClickListener {
|
||||
onSavingFile(filename, chatItem.data.sliceArray(1 until 17))
|
||||
}
|
||||
return itemView.findViewById(R.id.bubble_content)
|
||||
}
|
||||
}
|
||||
|
||||
internal class OutgoingFileViewHolder(private val context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(itemView, onSavingFile) {
|
||||
fun bind(chatItem: ChatItem) {
|
||||
configureBubble(context, bindFile(chatItem), true)
|
||||
setTimestamp(chatItem).apply {
|
||||
setTextColor(ContextCompat.getColor(context, R.color.outgoingTimestamp))
|
||||
}
|
||||
bindFile(chatItem)
|
||||
configureBubble(context, true)
|
||||
setPadding(true)
|
||||
}
|
||||
}
|
||||
|
||||
internal class IncomingFileViewHolder(private val context: Context, itemView: View, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): FileViewHolder(itemView, onSavingFile) {
|
||||
fun bind(chatItem: ChatItem) {
|
||||
configureBubble(context, bindFile(chatItem), false)
|
||||
setTimestamp(chatItem).apply {
|
||||
setTextColor(ContextCompat.getColor(context, R.color.incomingTimestamp))
|
||||
}
|
||||
bindFile(chatItem)
|
||||
configureBubble(context, false)
|
||||
setPadding(false)
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import androidx.core.app.RemoteInput
|
||||
import sushi.hardcore.aira.*
|
||||
import sushi.hardcore.aira.utils.FileUtils
|
||||
import sushi.hardcore.aira.utils.StringUtils
|
||||
import sushi.hardcore.aira.utils.TimeUtils
|
||||
import java.io.IOException
|
||||
import java.net.*
|
||||
import java.nio.channels.*
|
||||
@ -108,7 +109,7 @@ class AIRAService : Service() {
|
||||
fun onSessionDisconnect(sessionId: Int)
|
||||
fun onNameTold(sessionId: Int, name: String)
|
||||
fun onAvatarChanged(sessionId: Int, avatar: ByteArray?)
|
||||
fun onNewMessage(sessionId: Int, data: ByteArray): Boolean
|
||||
fun onNewMessage(sessionId: Int, timestamp: Long, data: ByteArray): Boolean
|
||||
fun onAskLargeFiles(sessionId: Int, filesReceiver: FilesReceiver): Boolean
|
||||
}
|
||||
|
||||
@ -164,7 +165,7 @@ class AIRAService : Service() {
|
||||
sendFile.inputStream.close()
|
||||
sendTo(sessionId, Protocol.newFile(sendFile.fileName, buffer))
|
||||
AIRADatabase.storeFile(contacts[sessionId]?.uuid, buffer)?.let { rawFileUuid ->
|
||||
saveMsg(sessionId, byteArrayOf(Protocol.FILE) + rawFileUuid + sendFile.fileName.toByteArray())
|
||||
saveMsg(sessionId, TimeUtils.getTimestamp(), byteArrayOf(Protocol.FILE) + rawFileUuid + sendFile.fileName.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
@ -203,7 +204,7 @@ class AIRAService : Service() {
|
||||
contacts[sessionId] = contact
|
||||
savedMsgs.remove(sessionId)?.let { msgs ->
|
||||
for (msg in msgs) {
|
||||
AIRADatabase.storeMsg(contact.uuid, msg.outgoing, msg.data)
|
||||
AIRADatabase.storeMsg(contact.uuid, msg.outgoing, msg.timestamp, msg.data)
|
||||
}
|
||||
}
|
||||
savedNames.remove(sessionId)
|
||||
@ -419,20 +420,20 @@ class AIRAService : Service() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveMsg(sessionId: Int, msg: ByteArray) {
|
||||
private fun saveMsg(sessionId: Int, timestamp: Long, msg: ByteArray) {
|
||||
var msgSaved = false
|
||||
contacts[sessionId]?.uuid?.let { uuid ->
|
||||
msgSaved = AIRADatabase.storeMsg(uuid, true, msg)
|
||||
msgSaved = AIRADatabase.storeMsg(uuid, true, timestamp, msg)
|
||||
}
|
||||
if (!msgSaved) {
|
||||
savedMsgs[sessionId]?.add(ChatItem(true, msg))
|
||||
savedMsgs[sessionId]?.add(ChatItem(true, timestamp, msg))
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendAndSave(sessionId: Int, msg: ByteArray) {
|
||||
sessions[sessionId]?.encryptAndSend(msg, usePadding)
|
||||
if (msg[0] == Protocol.MESSAGE) {
|
||||
saveMsg(sessionId, msg)
|
||||
saveMsg(sessionId, TimeUtils.getTimestamp(), msg)
|
||||
}
|
||||
}
|
||||
|
||||
@ -810,17 +811,18 @@ class AIRAService : Service() {
|
||||
null
|
||||
}
|
||||
}?.let { handledMsg ->
|
||||
val timestamp = TimeUtils.getTimestamp()
|
||||
var seen = false
|
||||
uiCallbacks?.let { uiCallbacks ->
|
||||
seen = uiCallbacks.onNewMessage(sessionId, handledMsg)
|
||||
seen = uiCallbacks.onNewMessage(sessionId, timestamp, handledMsg)
|
||||
}
|
||||
setSeen(sessionId, seen)
|
||||
var msgSaved = false
|
||||
contacts[sessionId]?.let { contact ->
|
||||
msgSaved = AIRADatabase.storeMsg(contact.uuid, false, handledMsg)
|
||||
msgSaved = AIRADatabase.storeMsg(contact.uuid, false, timestamp, handledMsg)
|
||||
}
|
||||
if (!msgSaved){
|
||||
savedMsgs[sessionId]?.add(ChatItem(false, handledMsg))
|
||||
savedMsgs[sessionId]?.add(ChatItem(false, timestamp, handledMsg))
|
||||
}
|
||||
if (isAppInBackground) {
|
||||
sendNotification(sessionId, handledMsg)
|
||||
|
@ -25,4 +25,12 @@ object StringUtils {
|
||||
fun sanitizeName(name: String): String {
|
||||
return name.replace('\n', ' ')
|
||||
}
|
||||
|
||||
fun toTwoDigits(number: Int): String {
|
||||
return if (number < 10) {
|
||||
"0$number"
|
||||
} else {
|
||||
number.toString()
|
||||
}
|
||||
}
|
||||
}
|
7
app/src/main/java/sushi/hardcore/aira/utils/TimeUtils.kt
Normal file
7
app/src/main/java/sushi/hardcore/aira/utils/TimeUtils.kt
Normal file
@ -0,0 +1,7 @@
|
||||
package sushi.hardcore.aira.utils
|
||||
|
||||
object TimeUtils {
|
||||
fun getTimestamp(): Long {
|
||||
return System.currentTimeMillis()/1000
|
||||
}
|
||||
}
|
@ -43,12 +43,11 @@ fn get_database_path(database_folder: &str) -> String {
|
||||
Path::new(database_folder).join(DB_NAME).to_str().unwrap().to_owned()
|
||||
}
|
||||
|
||||
struct EncryptedIdentity {
|
||||
name: String,
|
||||
encrypted_keypair: Vec<u8>,
|
||||
salt: Vec<u8>,
|
||||
encrypted_master_key: Vec<u8>,
|
||||
encrypted_use_padding: Vec<u8>,
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Message {
|
||||
pub outgoing: bool,
|
||||
pub timestamp: u64,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct Contact {
|
||||
@ -60,6 +59,14 @@ pub struct Contact {
|
||||
pub seen: bool,
|
||||
}
|
||||
|
||||
struct EncryptedIdentity {
|
||||
name: String,
|
||||
encrypted_keypair: Vec<u8>,
|
||||
salt: Vec<u8>,
|
||||
encrypted_master_key: Vec<u8>,
|
||||
encrypted_use_padding: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct Identity {
|
||||
pub name: String,
|
||||
keypair: Keypair,
|
||||
@ -251,16 +258,17 @@ impl Identity {
|
||||
Ok(file_uuid)
|
||||
}
|
||||
|
||||
pub fn store_msg(&self, contact_uuid: &Uuid, outgoing: bool, data: &[u8]) -> Result<usize, rusqlite::Error> {
|
||||
pub fn store_msg(&self, contact_uuid: &Uuid, message: Message) -> Result<usize, rusqlite::Error> {
|
||||
let db = Connection::open(self.get_database_path())?;
|
||||
db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (outgoing BLOB, data BLOB)", contact_uuid), [])?;
|
||||
let outgoing_byte: u8 = bool_to_byte(outgoing);
|
||||
db.execute(&format!("CREATE TABLE IF NOT EXISTS \"{}\" (outgoing BLOB, timestamp BLOB, data BLOB)", contact_uuid), [])?;
|
||||
let outgoing_byte: u8 = bool_to_byte(message.outgoing);
|
||||
let encrypted_outgoing = crypto::encrypt_data(&[outgoing_byte], &self.master_key).unwrap();
|
||||
let encrypted_data = crypto::encrypt_data(data, &self.master_key).unwrap();
|
||||
db.execute(&format!("INSERT INTO \"{}\" (outgoing, data) VALUES (?1, ?2)", contact_uuid), params![encrypted_outgoing, encrypted_data])
|
||||
let encrypted_timestamp = crypto::encrypt_data(&message.timestamp.to_be_bytes(), &self.master_key).unwrap();
|
||||
let encrypted_data = crypto::encrypt_data(&message.data, &self.master_key).unwrap();
|
||||
db.execute(&format!("INSERT INTO \"{}\" (outgoing, timestamp, data) VALUES (?1, ?2, ?3)", contact_uuid), params![encrypted_outgoing, encrypted_timestamp, encrypted_data])
|
||||
}
|
||||
|
||||
pub fn load_msgs(&self, contact_uuid: &Uuid, offset: usize, mut count: usize) -> Option<Vec<(bool, Vec<u8>)>> {
|
||||
pub fn load_msgs(&self, contact_uuid: &Uuid, offset: usize, mut count: usize) -> Option<Vec<Message>> {
|
||||
match Connection::open(self.get_database_path()) {
|
||||
Ok(db) => {
|
||||
if let Ok(mut stmt) = db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) {
|
||||
@ -271,7 +279,7 @@ impl Identity {
|
||||
if offset+count >= total {
|
||||
count = total-offset;
|
||||
}
|
||||
let mut stmt = db.prepare(&format!("SELECT outgoing, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)).unwrap();
|
||||
let mut stmt = db.prepare(&format!("SELECT outgoing, timestamp, data FROM \"{}\" LIMIT {} OFFSET {}", contact_uuid, count, total-offset-count)).unwrap();
|
||||
let mut rows = stmt.query([]).unwrap();
|
||||
let mut msgs = Vec::new();
|
||||
while let Ok(Some(row)) = rows.next() {
|
||||
@ -280,9 +288,19 @@ impl Identity {
|
||||
Ok(outgoing) => {
|
||||
match byte_to_bool(outgoing[0]) {
|
||||
Ok(outgoing) => {
|
||||
let encrypted_data: Vec<u8> = row.get(1).unwrap();
|
||||
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
|
||||
Ok(data) => msgs.push((outgoing, data)),
|
||||
let encrypted_timestamp: Vec<u8> = row.get(1).unwrap();
|
||||
match crypto::decrypt_data(&encrypted_timestamp, &self.master_key) {
|
||||
Ok(timestamp) => {
|
||||
let encrypted_data: Vec<u8> = row.get(2).unwrap();
|
||||
match crypto::decrypt_data(encrypted_data.as_slice(), &self.master_key) {
|
||||
Ok(data) => msgs.push(Message {
|
||||
outgoing,
|
||||
timestamp: u64::from_be_bytes(timestamp.try_into().unwrap()),
|
||||
data,
|
||||
}),
|
||||
Err(e) => print_error!(e)
|
||||
}
|
||||
}
|
||||
Err(e) => print_error!(e)
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use std::{convert::TryInto, fmt::Display, str::FromStr, sync::{Mutex}};
|
||||
use lazy_static::lazy_static;
|
||||
use uuid::Uuid;
|
||||
use android_log;
|
||||
use identity::{Identity, Contact};
|
||||
use identity::{Identity, Contact, Message};
|
||||
use crate::crypto::{HandshakeKeys, ApplicationKeys};
|
||||
|
||||
lazy_static! {
|
||||
@ -17,7 +17,7 @@ lazy_static! {
|
||||
#[cfg(target_os="android")]
|
||||
use jni::JNIEnv;
|
||||
use jni::objects::{JClass, JObject, JString, JList, JValue};
|
||||
use jni::sys::{jboolean, jint, jbyteArray, jobject};
|
||||
use jni::sys::{jboolean, jint, jlong, jbyteArray, jobject};
|
||||
|
||||
fn jstring_to_string(env: JNIEnv, input: JString) -> String {
|
||||
String::from(env.get_string(input).unwrap())
|
||||
@ -287,8 +287,13 @@ pub fn Java_sushi_hardcore_aira_AIRADatabase_setContactSeen(env: JNIEnv, _: JCla
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
pub fn Java_sushi_hardcore_aira_AIRADatabase_storeMsg(env: JNIEnv, _: JClass, contactUuid: JString, outgoing: jboolean, data: jbyteArray) -> jboolean {
|
||||
result_to_jboolean(loaded_identity.lock().unwrap().as_ref().unwrap().store_msg(&jstring_to_uuid(env, contactUuid).unwrap(), jboolean_to_bool(outgoing), &env.convert_byte_array(data).unwrap()))
|
||||
pub fn Java_sushi_hardcore_aira_AIRADatabase_storeMsg(env: JNIEnv, _: JClass, contactUuid: JString, outgoing: jboolean, timestamp: jlong, data: jbyteArray) -> jboolean {
|
||||
let message = Message {
|
||||
outgoing: jboolean_to_bool(outgoing),
|
||||
timestamp: timestamp as u64,
|
||||
data: env.convert_byte_array(data).unwrap(),
|
||||
};
|
||||
result_to_jboolean(loaded_identity.lock().unwrap().as_ref().unwrap().store_msg(&jstring_to_uuid(env, contactUuid).unwrap(), message))
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
@ -301,7 +306,11 @@ pub fn Java_sushi_hardcore_aira_AIRADatabase_loadMsgs(env: JNIEnv, _: JClass, uu
|
||||
let array_list = JList::from_env(&env, array_list).unwrap();
|
||||
let chat_item_class = env.find_class("sushi/hardcore/aira/ChatItem").unwrap();
|
||||
for msg in msgs {
|
||||
let chat_item_object = env.new_object(chat_item_class, "(Z[B)V", &[JValue::Bool(bool_to_jboolean(msg.0)), slice_to_jvalue(env, &msg.1)]).unwrap();
|
||||
let chat_item_object = env.new_object(chat_item_class, "(ZJ[B)V", &[
|
||||
JValue::Bool(bool_to_jboolean(msg.outgoing)),
|
||||
JValue::Long(msg.timestamp as jlong),
|
||||
slice_to_jvalue(env, &msg.data),
|
||||
]).unwrap();
|
||||
array_list.add(chat_item_object).unwrap();
|
||||
}
|
||||
*array_list
|
||||
|
@ -8,26 +8,38 @@
|
||||
android:id="@+id/bubble_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
style="@style/Bubble"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingVertical="5dp">
|
||||
android:orientation="vertical"
|
||||
style="@style/Bubble">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_save"
|
||||
android:layout_width="@dimen/image_button_size"
|
||||
android:layout_height="@dimen/image_button_size"
|
||||
android:src="@drawable/ic_save"
|
||||
style="@style/ImageButton"
|
||||
android:contentDescription="@string/download" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_filename"
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:textColor="@color/messageTextColor"/>
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_save"
|
||||
android:layout_width="@dimen/image_button_size"
|
||||
android:layout_height="@dimen/image_button_size"
|
||||
android:src="@drawable/ic_save"
|
||||
android:scaleType="fitXY"
|
||||
android:background="@color/transparent"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="@string/download" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_filename"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:textColor="@color/messageTextColor"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
@ -4,13 +4,27 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="1dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_message"
|
||||
<LinearLayout
|
||||
android:id="@+id/bubble_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:autoLink="all"
|
||||
android:textColor="@color/messageTextColor"
|
||||
style="@style/Bubble"/>
|
||||
android:orientation="vertical"
|
||||
style="@style/Bubble">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:autoLink="all"
|
||||
android:textColor="@color/messageTextColor"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
@ -12,4 +12,6 @@
|
||||
<color name="itemSelected">#66666666</color>
|
||||
<color name="transparent">#00000000</color>
|
||||
<color name="sessionArrow">#777777</color>
|
||||
<color name="outgoingTimestamp">#777777</color>
|
||||
<color name="incomingTimestamp">#d7d7d7</color>
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user