Profile toolbar & 4 ChatItem types & Other UI improvements

This commit is contained in:
Matéo Duparc 2021-05-21 14:55:37 +02:00
parent e77887a51c
commit 2f01c86359
Signed by: hardcoresushi
GPG Key ID: 007F84120107191E
18 changed files with 308 additions and 225 deletions

View File

@ -42,9 +42,9 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation "androidx.fragment:fragment-ktx:1.3.3"
implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation "androidx.fragment:fragment-ktx:1.3.4"
implementation "androidx.preference:preference-ktx:1.1.1"
implementation 'com.google.android.material:material:1.3.0'
//implementation 'androidx.constraintlayout:constraintlayout:2.0.4'

View File

@ -28,8 +28,8 @@
</intent-filter>
</receiver>
<activity android:name=".ChatActivity"/>
<activity android:name=".MainActivity">
<activity android:name=".ChatActivity" android:theme="@style/Theme.AIRA.NoActionBar"/>
<activity android:name=".MainActivity" android:theme="@style/Theme.AIRA.NoActionBar">
<intent-filter android:label="@string/share_label">
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.SEND_MULTIPLE"/>

View File

@ -1,16 +1,17 @@
package sushi.hardcore.aira
import android.content.ComponentName
import android.content.Context
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -32,14 +33,7 @@ class ChatActivity : ServiceBoundActivity() {
private var lastLoadedMessageOffset = 0
private val filePicker = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
if (isServiceInitialized() && uris.size > 0) {
airaService.sendFilesFromUris(sessionId, uris)?.let { msgs ->
for (msg in msgs) {
chatAdapter.newMessage(ChatItem(true, msg))
if (airaService.contacts.contains(sessionId)) {
lastLoadedMessageOffset += 1
}
}
}
airaService.sendFilesFromUris(sessionId, uris)
}
}
@ -47,13 +41,15 @@ class ChatActivity : ServiceBoundActivity() {
super.onCreate(savedInstanceState)
binding = ActivityChatBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
sessionId = intent.getIntExtra("sessionId", -1)
if (sessionId != -1) {
intent.getStringExtra("sessionName")?.let { name ->
sessionName = name
title = name
setSupportActionBar(binding.toolbar.toolbar)
binding.toolbar.textAvatar.setLetterFrom(name)
binding.toolbar.title.text = name
supportActionBar?.setDisplayHomeAsUpEnabled(true)
chatAdapter = ChatAdapter(this@ChatActivity, ::onClickSaveFile)
binding.recyclerChat.apply {
@ -112,7 +108,7 @@ class ChatActivity : ServiceBoundActivity() {
}
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
val showBottomPanel = {
findViewById<ConstraintLayout>(R.id.bottom_panel).visibility = View.VISIBLE
binding.bottomPanel.visibility = View.VISIBLE
}
airaService.uiCallbacks = object : AIRAService.UiCallbacks {
override fun onNewSession(sessionId: Int, ip: String) {
@ -125,7 +121,9 @@ class ChatActivity : ServiceBoundActivity() {
override fun onSessionDisconnect(sessionId: Int) {
if (this@ChatActivity.sessionId == sessionId) {
runOnUiThread {
findViewById<ConstraintLayout>(R.id.bottom_panel).visibility = View.GONE
val inputManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputManager.hideSoftInputFromWindow(binding.editMessage.windowToken, 0)
binding.bottomPanel.visibility = View.GONE
}
}
}

View File

@ -4,8 +4,14 @@ import sushi.hardcore.aira.background_service.Protocol
class ChatItem(val outgoing: Boolean, val data: ByteArray) {
companion object {
const val MESSAGE = 0
const val FILE = 1
const val OUTGOING_MESSAGE = 0
const val INCOMING_MESSAGE = 1
const val OUTGOING_FILE = 2
const val INCOMING_FILE = 3
}
val itemType = if (data[0] == Protocol.MESSAGE) {
if (outgoing) OUTGOING_MESSAGE else INCOMING_MESSAGE
} else {
if (outgoing) OUTGOING_FILE else INCOMING_FILE
}
val itemType = if (data[0] == Protocol.MESSAGE) { MESSAGE } else { FILE }
}

View File

@ -79,7 +79,11 @@ class MainActivity : ServiceBoundActivity() {
setContentView(binding.root)
val identityName = intent.getStringExtra("identityName")
identityName?.let { title = it }
identityName?.let {
setSupportActionBar(binding.toolbar.toolbar)
binding.toolbar.textAvatar.setLetterFrom(it)
binding.toolbar.title.text = it
}
val openedToShareFile = intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE

View File

@ -11,7 +11,6 @@ import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.marginEnd
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.RecyclerView
import sushi.hardcore.aira.ChatItem
@ -23,7 +22,8 @@ class ChatAdapter(
): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
const val BUBBLE_MARGIN = 70
const val CONTAINER_MARGIN = 70
const val BUBBLE_HORIZONTAL_PADDING = 40
}
private val inflater: LayoutInflater = LayoutInflater.from(context)
@ -44,88 +44,106 @@ class ChatAdapter(
notifyDataSetChanged()
}
internal open class BubbleViewHolder(private val context: Context, itemView: View): RecyclerView.ViewHolder(itemView) {
fun handleItemView(position: Int) {
itemView.updatePadding(top = if (position == 0) {
50
} else {
itemView.paddingBottom
})
}
fun setBubbleColor(bubble: View, outgoing: Boolean) {
internal open class BubbleViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
protected fun setPadding(outgoing: Boolean) {
if (outgoing) {
bubble.background.clearColorFilter()
itemView.updatePadding(right = BUBBLE_HORIZONTAL_PADDING)
} else {
bubble.background.colorFilter = PorterDuffColorFilter(ContextCompat.getColor(context, R.color.incomingBubbleBackground), PorterDuff.Mode.SRC)
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 {
gravity = if (outgoing) {
marginStart = CONTAINER_MARGIN
Gravity.END
} else {
marginEnd = CONTAINER_MARGIN
Gravity.START
}
}
if (!outgoing) {
view.background.colorFilter = PorterDuffColorFilter(
ContextCompat.getColor(context, R.color.incomingBubbleBackground),
PorterDuff.Mode.SRC
)
}
}
}
internal class MessageViewHolder(context: Context, itemView: View): BubbleViewHolder(context, itemView) {
fun bind(chatItem: ChatItem, position: Int) {
internal open class MessageViewHolder(itemView: View): BubbleViewHolder(itemView) {
protected fun bindMessage(chatItem: ChatItem): TextView {
itemView.findViewById<TextView>(R.id.text_message).apply {
text = chatItem.data.sliceArray(1 until chatItem.data.size).decodeToString()
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
if (chatItem.outgoing) {
gravity = Gravity.END
marginStart = BUBBLE_MARGIN
} else {
gravity = Gravity.START
marginEnd = BUBBLE_MARGIN
return this
}
}
setBubbleColor(this, chatItem.outgoing)
}
handleItemView(position)
}
}
internal class FileViewHolder(context: Context, itemView: View): BubbleViewHolder(context, itemView) {
fun bind(chatItem: ChatItem, position: Int, onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit) {
internal class OutgoingMessageViewHolder(private val context: Context, itemView: View): MessageViewHolder(itemView) {
fun bind(chatItem: ChatItem) {
configureBubble(context, bindMessage(chatItem), true)
setPadding(true)
}
}
internal class IncomingMessageViewHolder(private val context: Context, itemView: View): MessageViewHolder(itemView) {
fun bind(chatItem: ChatItem) {
configureBubble(context, bindMessage(chatItem), 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 {
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))
}
itemView.findViewById<LinearLayout>(R.id.bubble_content).apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
if (chatItem.outgoing) {
gravity = Gravity.END
marginStart = BUBBLE_MARGIN
} else {
gravity = Gravity.START
marginEnd = BUBBLE_MARGIN
return itemView.findViewById(R.id.bubble_content)
}
}
setBubbleColor(this, chatItem.outgoing)
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)
setPadding(true)
}
handleItemView(position)
}
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)
setPadding(false)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ChatItem.MESSAGE -> {
return if (viewType == ChatItem.OUTGOING_MESSAGE || viewType == ChatItem.INCOMING_MESSAGE) {
val view = inflater.inflate(R.layout.adapter_chat_message, parent, false)
MessageViewHolder(context, view)
if (viewType == ChatItem.OUTGOING_MESSAGE) {
OutgoingMessageViewHolder(context, view)
} else {
IncomingMessageViewHolder(context, view)
}
ChatItem.FILE -> {
} else {
val view = inflater.inflate(R.layout.adapter_chat_file, parent, false)
FileViewHolder(context, view)
if (viewType == ChatItem.OUTGOING_FILE) {
OutgoingFileViewHolder(context, view, onSavingFile)
} else {
IncomingFileViewHolder(context, view, onSavingFile)
}
else -> throw RuntimeException("Invalid chat item type")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val chatItem = chatItems[position]
when (chatItem.itemType) {
ChatItem.MESSAGE -> {
(holder as MessageViewHolder).bind(chatItem, position)
}
ChatItem.FILE -> {
(holder as FileViewHolder).bind(chatItem, position, onSavingFile)
}
ChatItem.OUTGOING_MESSAGE -> (holder as OutgoingMessageViewHolder).bind(chatItem)
ChatItem.INCOMING_MESSAGE -> (holder as IncomingMessageViewHolder).bind(chatItem)
ChatItem.OUTGOING_FILE -> (holder as OutgoingFileViewHolder).bind(chatItem)
ChatItem.INCOMING_FILE -> (holder as IncomingFileViewHolder).bind(chatItem)
}
}

View File

@ -135,7 +135,7 @@ class AIRAService : Service() {
}
}
fun sendFilesFromUris(sessionId: Int, uris: List<Uri>): List<ByteArray>? {
fun sendFilesFromUris(sessionId: Int, uris: List<Uri>) {
val files = mutableListOf<SendFile>()
var useLargeFileTransfer = false
for (uri in uris) {
@ -146,30 +146,22 @@ class AIRAService : Service() {
}
}
}
return if (useLargeFileTransfer) {
if (useLargeFileTransfer) {
sendLargeFilesTo(sessionId, files)
null
} else {
val msgs = mutableListOf<ByteArray>()
for (file in files) {
sendSmallFileTo(sessionId, file)?.let { msg ->
msgs.add(msg)
sendSmallFileTo(sessionId, file)
}
}
msgs
}
}
private fun sendSmallFileTo(sessionId: Int, sendFile: SendFile): ByteArray? {
private fun sendSmallFileTo(sessionId: Int, sendFile: SendFile) {
val buffer = sendFile.inputStream.readBytes()
sendFile.inputStream.close()
sendTo(sessionId, Protocol.newFile(sendFile.fileName, buffer))
AIRADatabase.storeFile(contacts[sessionId]?.uuid, buffer)?.let { rawFileUuid ->
val msg = byteArrayOf(Protocol.FILE) + rawFileUuid + sendFile.fileName.toByteArray()
saveMsg(sessionId, msg)
return msg
saveMsg(sessionId, byteArrayOf(Protocol.FILE) + rawFileUuid + sendFile.fileName.toByteArray())
}
return null
}
private fun sendLargeFilesTo(sessionId: Int, files: MutableList<SendFile>) {

View File

@ -11,10 +11,10 @@ jni = { version = "0.19", default-features = false }
crate-type = ["dylib"]
[dependencies]
rand-8 = {package = "rand", version = "0.8.3"}
rand = "0.8.3"
rand-7 = {package = "rand", version = "0.7.3"}
lazy_static = "1.4.0"
rusqlite = {version = "0.25.1", features = ["bundled"]}
rusqlite = { version = "0.25.1", features = ["bundled"] }
ed25519-dalek = "1" #for singing
x25519-dalek = "1.1" #for shared secret
sha2 = "0.9.3"
@ -24,7 +24,7 @@ aes-gcm-siv = "0.10.0" #Database
hmac = "0.11.0"
hex = "0.4.3"
strum_macros = "0.20.1" #display enums
uuid = {version = "0.8", features = ["v4"]}
uuid = { version = "0.8", features = ["v4"] }
scrypt = "0.7.0"
zeroize = "1.2.0"
log = "0.4.14"

View File

@ -1,14 +1,12 @@
use std::convert::TryInto;
use std::{convert::TryInto, fmt::Display};
use hkdf::Hkdf;
use sha2::Sha384;
use hmac::{Hmac, Mac, NewMac};
use hmac::{Hmac, NewMac, Mac};
use scrypt::{scrypt, Params};
use rand_8::{RngCore, rngs::OsRng};
use rand::{RngCore, rngs::OsRng};
use aes_gcm::{aead::Aead, NewAead, Nonce};
use aes_gcm_siv::Aes256GcmSiv;
use zeroize::Zeroize;
use strum_macros::Display;
use crate::utils::*;
pub const HASH_OUTPUT_LEN: usize = 48; //SHA384
const KEY_LEN: usize = 16;
@ -33,6 +31,26 @@ fn hkdf_expand_label(key: &[u8], label: &str, context: Option<&[u8]>, okm: &mut
};
}
fn get_labels(handshake: bool, i_am_bob: bool) -> (String, String) {
let mut label = if handshake {
"handshake"
} else {
"application"
}.to_owned();
label += "_i_am_";
let local_label = label.clone() + if i_am_bob {
"bob"
} else {
"alice"
};
let peer_label = label + if i_am_bob {
"alice"
} else {
"bob"
};
(local_label, peer_label)
}
pub struct HandshakeKeys {
pub local_key: [u8; KEY_LEN],
pub local_iv: [u8; IV_LEN],
@ -47,11 +65,11 @@ impl HandshakeKeys {
pub fn derive_keys(shared_secret: [u8; 32], handshake_hash: [u8; HASH_OUTPUT_LEN], i_am_bob: bool) -> HandshakeKeys {
let (handshake_secret, _) = Hkdf::<Sha384>::extract(None, &shared_secret);
let local_label = "handshake".to_owned() + if i_am_bob {"i_am_bob"} else {"i_am_alice"};
let (local_label, peer_label) = get_labels(true, i_am_bob);
let mut local_handshake_traffic_secret = [0; HASH_OUTPUT_LEN];
hkdf_expand_label(handshake_secret.as_slice(), &local_label, Some(&handshake_hash), &mut local_handshake_traffic_secret);
let peer_label = "handshake".to_owned() + if i_am_bob {"i_am_alice"} else {"i_am_bob"};
let mut peer_handshake_traffic_secret = [0; HASH_OUTPUT_LEN];
hkdf_expand_label(handshake_secret.as_slice(), &peer_label, Some(&handshake_hash), &mut peer_handshake_traffic_secret);
@ -72,7 +90,7 @@ impl HandshakeKeys {
peer_key: peer_handshake_key,
peer_iv: peer_handshake_iv,
peer_handshake_traffic_secret: peer_handshake_traffic_secret,
handshake_secret: to_array_48(handshake_secret.as_slice())
handshake_secret: handshake_secret.as_slice().try_into().unwrap(),
}
}
}
@ -90,11 +108,11 @@ impl ApplicationKeys {
hkdf_expand_label(&handshake_secret, "derived", None, &mut derived_secret);
let (master_secret, _) = Hkdf::<Sha384>::extract(Some(&derived_secret), b"");
let local_label = "application".to_owned() + if i_am_bob {"i_am_bob"} else {"i_am_alice"};
let (local_label, peer_label) = get_labels(false, i_am_bob);
let mut local_application_traffic_secret = [0; HASH_OUTPUT_LEN];
hkdf_expand_label(&master_secret, &local_label, Some(&handshake_hash), &mut local_application_traffic_secret);
let peer_label = "application".to_owned() + if i_am_bob {"i_am_alice"} else {"i_am_bob"};
let mut peer_application_traffic_secret = [0; HASH_OUTPUT_LEN];
hkdf_expand_label(&master_secret, &peer_label, Some(&handshake_hash), &mut peer_application_traffic_secret);
@ -161,12 +179,21 @@ pub fn encrypt_data(data: &[u8], master_key: &[u8]) -> Result<Vec<u8>, CryptoErr
Ok(cipher_text)
}
#[derive(Display, Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq)]
pub enum CryptoError {
DecryptionFailed,
InvalidLength
}
impl Display for CryptoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
CryptoError::DecryptionFailed => "Decryption failed",
CryptoError::InvalidLength => "Invalid length",
})
}
}
pub fn decrypt_data(data: &[u8], master_key: &[u8]) -> Result<Vec<u8>, CryptoError> {
if data.len() <= IV_LEN || master_key.len() != MASTER_KEY_LEN {
return Err(CryptoError::InvalidLength);

View File

@ -256,11 +256,10 @@ impl Identity {
match db.prepare(&format!("SELECT count(*) FROM \"{}\"", contact_uuid)) {
Ok(mut stmt) => {
let mut rows = stmt.query([]).unwrap();
let row = rows.next().unwrap();
if row.is_some() {
match rows.next() {
Ok(row) => if row.is_some() {
let total: usize = row.unwrap().get(0).unwrap();
if offset >= total {
print_error!("Offset larger than total numbers of rows");
None
} else {
if offset+count >= total {
@ -307,6 +306,8 @@ impl Identity {
} else {
None
}
Err(_) => None
}
}
Err(e) => {
print_error!(e);

View File

@ -2,10 +2,6 @@ use std::convert::TryInto;
use uuid::Bytes;
use crate::print_error;
pub fn to_array_48(s: &[u8]) -> [u8; 48] {
s.try_into().unwrap()
}
pub fn to_uuid_bytes(bytes: &[u8]) -> Option<Bytes> {
match bytes.try_into() {
Ok(uuid) => Some(uuid),

View File

@ -6,13 +6,13 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".ChatActivity">
<include layout="@layout/profile_toolbar" android:id="@+id/toolbar"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_chat"
android:layout_width="match_parent"
android:layout_height="0dp"
android:paddingHorizontal="20dp"
android:paddingBottom="20dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toTopOf="@id/bottom_panel"/>
<androidx.constraintlayout.widget.ConstraintLayout
@ -20,7 +20,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
android:visibility="gone">
android:visibility="gone"
android:paddingVertical="5dp"
android:background="@color/primary">
<ImageButton
android:id="@+id/button_attach"

View File

@ -1,12 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/refresher"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".MainActivity">
<include layout="@layout/profile_toolbar" android:id="@+id/toolbar"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresher"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -23,6 +31,7 @@
android:id="@+id/online_sessions"
android:layout_width="match_parent"
android:layout_height="0dp"
android:divider="@color/transparent"
app:layout_constraintTop_toBottomOf="@id/text_online_sessions"/>
<TextView
@ -37,6 +46,7 @@
android:id="@+id/offline_sessions"
android:layout_width="match_parent"
android:layout_height="0dp"
android:divider="@color/transparent"
app:layout_constraintTop_toBottomOf="@id/text_offline_sessions"
app:layout_constraintBottom_toTopOf="@+id/bottom_panel"/>
@ -45,8 +55,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginHorizontal="10dp"
app:layout_constraintBottom_toBottomOf="parent">
app:layout_constraintBottom_toBottomOf="parent"
android:paddingHorizontal="10dp"
android:paddingVertical="5dp"
android:background="@color/primary">
<EditText
android:id="@+id/edit_peer_ip"
@ -76,4 +88,6 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -4,7 +4,7 @@
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp">
android:padding="15dp">
<sushi.hardcore.aira.widgets.TextAvatar
android:id="@+id/text_avatar"

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/primary"
app:contentInsetStartWithNavigation="0dp"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="match_parent">
<sushi.hardcore.aira.widgets.TextAvatar
android:id="@+id/text_avatar"
android:layout_width="40dp"
android:layout_height="40dp"
app:textSize="8sp"
android:layout_gravity="center_vertical"/>
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
android:layout_marginStart="10dp"
android:layout_gravity="center_vertical"/>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.Toolbar>

View File

@ -4,7 +4,7 @@
<color name="secondary">#19a52c</color>
<color name="white">#FFFFFF</color>
<color name="backgroundColor">#111111</color>
<color name="sessionBackground">#1F1F1F</color>
<color name="sessionBackground">#1A1A1A</color>
<color name="bubbleBackground">@color/sessionBackground</color>
<color name="incomingBubbleBackground">@color/secondary</color>
<color name="textLink">#3845A3</color>

View File

@ -1,18 +1,11 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.AIRA" parent="Theme.AppCompat">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryVariant">@color/primary</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/secondary</item>
<item name="colorSecondaryVariant">@color/secondary</item>
<item name="colorAccent">@color/secondary</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<item name="android:windowBackground">@color/backgroundColor</item>
</style>
</resources>

View File

@ -1,12 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.4.32"
ext.kotlin_version = "1.5.0"
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.0'
classpath 'com.android.tools.build:gradle:4.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
@ -17,7 +17,7 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}