├── .gitignore
├── LICENSE.txt
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── ibashkimi
│ │ └── telegram
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── ibashkimi
│ │ │ └── telegram
│ │ │ ├── MainActivity.kt
│ │ │ ├── Navigation.kt
│ │ │ ├── TelegramApplication.kt
│ │ │ ├── data
│ │ │ ├── Authentication.kt
│ │ │ ├── Response.kt
│ │ │ ├── TelegramClient.kt
│ │ │ ├── UserRepository.kt
│ │ │ ├── chats
│ │ │ │ ├── ChatsPagingSource.kt
│ │ │ │ └── ChatsRepository.kt
│ │ │ └── messages
│ │ │ │ ├── MessagesPagingSource.kt
│ │ │ │ └── MessagesRepository.kt
│ │ │ ├── di
│ │ │ └── AppModule.kt
│ │ │ └── ui
│ │ │ ├── TelegramApp.kt
│ │ │ ├── chat
│ │ │ ├── ChatScreen.kt
│ │ │ ├── ChatScreenViewModel.kt
│ │ │ └── MessageItem.kt
│ │ │ ├── createchat
│ │ │ ├── CreateChatScreen.kt
│ │ │ └── CreateChatViewModel.kt
│ │ │ ├── home
│ │ │ ├── ChatItem.kt
│ │ │ ├── ChatListScreen.kt
│ │ │ ├── Drawer.kt
│ │ │ ├── HomeViewModel.kt
│ │ │ └── MainScreen.kt
│ │ │ ├── login
│ │ │ ├── LoginScreen.kt
│ │ │ └── LoginViewModel.kt
│ │ │ ├── theme
│ │ │ └── Theme.kt
│ │ │ └── util
│ │ │ └── Image.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ └── ic_person.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── ibashkimi
│ └── telegram
│ └── ExampleUnitTest.java
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | # Local configuration file (sdk path, etc)
2 | local.properties
3 |
4 | libtd/
5 |
6 | # Gradle generated files
7 | .gradle/
8 | build
9 | app/release
10 |
11 | # Signing files
12 | .signing/
13 |
14 | # User-specific configurations
15 | .idea/*
16 | *.iml
17 |
18 | # OS-specific files
19 | .DS_Store
20 | .DS_Store?
21 | ._*
22 | .Spotlight-V100
23 | .Trashes
24 | ehthumbs.db
25 | Thumbs.db
26 | .directory
27 |
28 | # Keystore files
29 | /screenshots/.directory
30 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.apache.org/licenses/LICENSE-2.0.html)
2 |
3 | # Telegram Example
4 |
5 | A telegram client for android created using tdlib library and built with [Jetpack Compose](https://developer.android.com/jetpack/compose).
6 |
7 | To try out this app, you need to:
8 | * Install the latest **Canary** of Android Studio
9 | * Download the **tdlib** android library from [https://core.telegram.org/tdlib/tdlib.zip](https://core.telegram.org/tdlib/tdlib.zip) and extract the zip file to the root folder of the project
10 | * Obtain application identifier hash for Telegram API access at [https://my.telegram.org](https://my.telegram.org) and store them in the android resources. For example in values/api_keys.xml:
11 | ```
12 |
13 | your integer api id
14 | your string api hash
15 |
16 | ```
17 |
18 | This app is **work in progress**. Features implemented so far:
19 | - [x] Login
20 | - [x] Show chat list
21 | - [ ] Show chat messages
22 | - [ ] Send messages
23 | - [ ] ...
24 |
25 | ## License
26 | Copyright (c) 2020 Indrit Bashkimi
27 |
28 | Licensed under the Apache License, Version 2.0 (the "License");
29 | you may not use this file except in compliance with the License.
30 | You may obtain a copy of the License at
31 |
32 | http://www.apache.org/licenses/LICENSE-2.0
33 |
34 | Unless required by applicable law or agreed to in writing, software
35 | distributed under the License is distributed on an "AS IS" BASIS,
36 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
37 | See the License for the specific language governing permissions and
38 | limitations under the License.
39 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | src/main/res/values/api_keys.xml
3 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-kapt'
4 | apply plugin: 'dagger.hilt.android.plugin'
5 |
6 | android {
7 | compileSdkVersion rootProject.ext.compileSdkVersion
8 | buildToolsVersion rootProject.ext.buildToolsVersion
9 | defaultConfig {
10 | applicationId "com.ibashkimi.telegram"
11 | minSdkVersion rootProject.ext.minSdkVersion
12 | targetSdkVersion rootProject.ext.targetSdkVersion
13 | versionCode 1
14 | versionName "1.0"
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | }
17 | buildTypes {
18 | release {
19 | minifyEnabled true
20 | shrinkResources true
21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | compileOptions {
25 | sourceCompatibility = 1.8
26 | targetCompatibility = 1.8
27 | }
28 | buildFeatures {
29 | compose true
30 | }
31 | kotlinOptions {
32 | jvmTarget = "1.8"
33 | freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
34 | }
35 | composeOptions {
36 | kotlinCompilerVersion "$kotlinVersion"
37 | kotlinCompilerExtensionVersion "$compose"
38 | }
39 | }
40 |
41 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
42 | kotlinOptions {
43 | jvmTarget = "1.8"
44 | }
45 | }
46 |
47 | dependencies {
48 | testImplementation 'junit:junit:4.13.2'
49 | androidTestImplementation 'androidx.test:runner:1.3.0'
50 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
51 |
52 | implementation fileTree(include: ['*.jar'], dir: 'libs')
53 | implementation project(':libtd')
54 |
55 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
56 | implementation 'com.google.android.material:material:1.3.0'
57 | implementation 'androidx.appcompat:appcompat:1.3.0-rc01'
58 | implementation "androidx.core:core-ktx:1.3.2"
59 |
60 | implementation "androidx.compose.runtime:runtime:$compose"
61 | implementation "androidx.activity:activity-compose:1.3.0-alpha07"
62 | implementation "androidx.compose.ui:ui:$compose"
63 | implementation "androidx.compose.ui:ui-tooling:$compose"
64 | implementation "androidx.compose.material:material:$compose"
65 | implementation "androidx.compose.material:material-icons-extended:$compose"
66 |
67 | implementation "androidx.navigation:navigation-compose:1.0.0-alpha10"
68 | implementation "androidx.paging:paging-compose:1.0.0-alpha08"
69 | implementation "androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha04"
70 | implementation 'com.google.accompanist:accompanist-coil:0.9.0'
71 |
72 | implementation "com.google.dagger:hilt-android:$hilt_version"
73 | kapt "com.google.dagger:hilt-compiler:$hilt_version"
74 | implementation 'androidx.hilt:hilt-navigation-compose:1.0.0-alpha01'
75 | }
76 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/ibashkimi/telegram/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram;
2 |
3 | import android.content.Context;
4 | import androidx.test.InstrumentationRegistry;
5 | import androidx.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumented test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.ibashkimi.telegram", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ibashkimi/telegram/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram
2 |
3 | import android.os.Bundle
4 | import androidx.activity.compose.setContent
5 | import androidx.appcompat.app.AppCompatActivity
6 | import com.ibashkimi.telegram.ui.TelegramApp
7 | import dagger.hilt.android.AndroidEntryPoint
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 |
10 | @AndroidEntryPoint
11 | class MainActivity : AppCompatActivity() {
12 |
13 | @OptIn(ExperimentalCoroutinesApi::class)
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | setContent {
17 | TelegramApp(this)
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ibashkimi/telegram/Navigation.kt:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram
2 |
3 | import androidx.navigation.NavBackStackEntry
4 |
5 | /**
6 | * Class defining the screens we have in the app: home, article details and interests
7 | */
8 | sealed class Screen(val route: String) {
9 |
10 | object Home : Screen("home")
11 |
12 | object Login : Screen("login")
13 |
14 | object Chat : Screen("chat/{chatId}") {
15 | fun buildRoute(chatId: Long): String = "chat/${chatId}"
16 | fun getChatId(entry: NavBackStackEntry): Long =
17 | entry.arguments!!.getString("chatId")?.toLong()
18 | ?: throw IllegalArgumentException("chatId argument missing.")
19 | }
20 |
21 | object CreateChat : Screen("createChat")
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ibashkimi/telegram/TelegramApplication.kt:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class TelegramApplication : Application()
--------------------------------------------------------------------------------
/app/src/main/java/com/ibashkimi/telegram/data/Authentication.kt:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram.data
2 |
3 | enum class Authentication {
4 | UNAUTHENTICATED,
5 | WAIT_FOR_NUMBER,
6 | WAIT_FOR_CODE,
7 | WAIT_FOR_PASSWORD,
8 | AUTHENTICATED,
9 | UNKNOWN
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ibashkimi/telegram/data/Response.kt:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram.data
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.flow.Flow
5 | import kotlinx.coroutines.flow.catch
6 | import kotlinx.coroutines.flow.flowOn
7 | import kotlinx.coroutines.flow.map
8 |
9 | /**
10 | * A generic class that holds a value or an exception
11 | */
12 | sealed class Response {
13 | data class Success(val data: T) : Response()
14 | data class Error(val exception: Throwable) : Response()
15 | }
16 |
17 | fun Flow.asResponse(): Flow> = map> { Response.Success(it) }
18 | .catch { emit(Response.Error(it)) }
19 | .flowOn(Dispatchers.IO)
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ibashkimi/telegram/data/TelegramClient.kt:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram.data
2 |
3 | import android.util.Log
4 | import kotlinx.coroutines.*
5 | import kotlinx.coroutines.channels.awaitClose
6 | import kotlinx.coroutines.flow.*
7 | import org.drinkless.td.libcore.telegram.Client
8 | import org.drinkless.td.libcore.telegram.TdApi
9 | import javax.inject.Inject
10 |
11 | /*
12 | * Go to https://my.telegram.org to obtain api id (integer) and api hash (string).
13 | * Put those in values (for example in values/api_keys.xml):
14 | *
15 | * your integer api id
16 | * your string api hash
17 | *
18 | */
19 | @OptIn(ExperimentalCoroutinesApi::class)
20 | class TelegramClient @Inject constructor(
21 | private val tdLibParameters: TdApi.TdlibParameters
22 | ) : Client.ResultHandler {
23 |
24 | private val TAG = TelegramClient::class.java.simpleName
25 |
26 | val client = Client.create(this, null, null)
27 |
28 | private val _authState = MutableStateFlow(Authentication.UNKNOWN)
29 | val authState: StateFlow get() = _authState
30 |
31 | init {
32 | client.send(TdApi.SetLogVerbosityLevel(1), this)
33 | client.send(TdApi.GetAuthorizationState(), this)
34 | }
35 |
36 | fun close() {
37 | client.close()
38 | }
39 |
40 | private val requestScope = CoroutineScope(Dispatchers.IO)
41 |
42 | private fun setAuth(auth: Authentication) {
43 | _authState.value = auth
44 | }
45 |
46 | override fun onResult(data: TdApi.Object) {
47 | Log.d(TAG, "onResult: ${data::class.java.simpleName}")
48 | when (data.constructor) {
49 | TdApi.UpdateAuthorizationState.CONSTRUCTOR -> {
50 | Log.d(TAG, "UpdateAuthorizationState")
51 | onAuthorizationStateUpdated((data as TdApi.UpdateAuthorizationState).authorizationState)
52 | }
53 | TdApi.UpdateOption.CONSTRUCTOR -> {
54 |
55 | }
56 |
57 | else -> Log.d(TAG, "Unhandled onResult call with data: $data.")
58 | }
59 | }
60 |
61 | private fun doAsync(job: () -> Unit) {
62 | requestScope.launch { job() }
63 | }
64 |
65 | fun startAuthentication() {
66 | Log.d(TAG, "startAuthentication called")
67 | if (_authState.value != Authentication.UNAUTHENTICATED) {
68 | throw IllegalStateException("Start authentication called but client already authenticated. State: ${_authState.value}.")
69 | }
70 |
71 | doAsync {
72 | client.send(TdApi.SetTdlibParameters(tdLibParameters)) {
73 | Log.d(TAG, "SetTdlibParameters result: $it")
74 | when (it.constructor) {
75 | TdApi.Ok.CONSTRUCTOR -> {
76 | //result.postValue(true)
77 | }
78 | TdApi.Error.CONSTRUCTOR -> {
79 | //result.postValue(false)
80 | }
81 | }
82 | }
83 | }
84 | }
85 |
86 | fun insertPhoneNumber(phoneNumber: String) {
87 | Log.d("TelegramClient", "phoneNumber: $phoneNumber")
88 | val settings = TdApi.PhoneNumberAuthenticationSettings(
89 | false,
90 | false,
91 | false
92 | )
93 | client.send(TdApi.SetAuthenticationPhoneNumber(phoneNumber, settings)) {
94 | Log.d("TelegramClient", "phoneNumber. result: $it")
95 | when (it.constructor) {
96 | TdApi.Ok.CONSTRUCTOR -> {
97 |
98 | }
99 | TdApi.Error.CONSTRUCTOR -> {
100 |
101 | }
102 | }
103 | }
104 | }
105 |
106 | fun insertCode(code: String) {
107 | Log.d("TelegramClient", "code: $code")
108 | doAsync {
109 | client.send(TdApi.CheckAuthenticationCode(code)) {
110 | when (it.constructor) {
111 | TdApi.Ok.CONSTRUCTOR -> {
112 |
113 | }
114 | TdApi.Error.CONSTRUCTOR -> {
115 |
116 | }
117 | }
118 | }
119 | }
120 | }
121 |
122 | fun insertPassword(password: String) {
123 | Log.d("TelegramClient", "inserting password")
124 | doAsync {
125 | client.send(TdApi.CheckAuthenticationPassword(password)) {
126 | when (it.constructor) {
127 | TdApi.Ok.CONSTRUCTOR -> {
128 |
129 | }
130 | TdApi.Error.CONSTRUCTOR -> {
131 |
132 | }
133 | }
134 | }
135 | }
136 | }
137 |
138 | private fun onAuthorizationStateUpdated(authorizationState: TdApi.AuthorizationState) {
139 | when (authorizationState.constructor) {
140 | TdApi.AuthorizationStateWaitTdlibParameters.CONSTRUCTOR -> {
141 | Log.d(
142 | TAG,
143 | "onResult: AuthorizationStateWaitTdlibParameters -> state = UNAUTHENTICATED"
144 | )
145 | setAuth(Authentication.UNAUTHENTICATED)
146 | }
147 | TdApi.AuthorizationStateWaitEncryptionKey.CONSTRUCTOR -> {
148 | Log.d(TAG, "onResult: AuthorizationStateWaitEncryptionKey")
149 | client.send(TdApi.CheckDatabaseEncryptionKey()) {
150 | when (it.constructor) {
151 | TdApi.Ok.CONSTRUCTOR -> {
152 | Log.d(TAG, "CheckDatabaseEncryptionKey: OK")
153 | }
154 | TdApi.Error.CONSTRUCTOR -> {
155 | Log.d(TAG, "CheckDatabaseEncryptionKey: Error")
156 | }
157 | }
158 | }
159 | }
160 | TdApi.AuthorizationStateWaitPhoneNumber.CONSTRUCTOR -> {
161 | Log.d(TAG, "onResult: AuthorizationStateWaitPhoneNumber -> state = WAIT_FOR_NUMBER")
162 | setAuth(Authentication.WAIT_FOR_NUMBER)
163 | }
164 | TdApi.AuthorizationStateWaitCode.CONSTRUCTOR -> {
165 | Log.d(TAG, "onResult: AuthorizationStateWaitCode -> state = WAIT_FOR_CODE")
166 | setAuth(Authentication.WAIT_FOR_CODE)
167 | }
168 | TdApi.AuthorizationStateWaitPassword.CONSTRUCTOR -> {
169 | Log.d(TAG, "onResult: AuthorizationStateWaitPassword")
170 | setAuth(Authentication.WAIT_FOR_PASSWORD)
171 | }
172 | TdApi.AuthorizationStateReady.CONSTRUCTOR -> {
173 | Log.d(TAG, "onResult: AuthorizationStateReady -> state = AUTHENTICATED")
174 | setAuth(Authentication.AUTHENTICATED)
175 | }
176 | TdApi.AuthorizationStateLoggingOut.CONSTRUCTOR -> {
177 | Log.d(TAG, "onResult: AuthorizationStateLoggingOut")
178 | setAuth(Authentication.UNAUTHENTICATED)
179 | }
180 | TdApi.AuthorizationStateClosing.CONSTRUCTOR -> {
181 | Log.d(TAG, "onResult: AuthorizationStateClosing")
182 | }
183 | TdApi.AuthorizationStateClosed.CONSTRUCTOR -> {
184 | Log.d(TAG, "onResult: AuthorizationStateClosed")
185 | }
186 | else -> Log.d(TAG, "Unhandled authorizationState with data: $authorizationState.")
187 | }
188 | }
189 |
190 | fun downloadableFile(file: TdApi.File): Flow =
191 | file.takeIf {
192 | it.local?.isDownloadingCompleted == false
193 | }?.id?.let { fileId ->
194 | downloadFile(fileId).map { file.local?.path }
195 | } ?: flowOf(file.local?.path)
196 |
197 | fun downloadFile(fileId: Int): Flow = callbackFlow {
198 | client.send(TdApi.DownloadFile(fileId, 1, 0, 0, true)) {
199 | when (it.constructor) {
200 | TdApi.Ok.CONSTRUCTOR -> {
201 | offer(Unit)
202 | }
203 | else -> {
204 | cancel("", Exception(""))
205 |
206 | }
207 | }
208 | }
209 | awaitClose()
210 | }
211 |
212 | fun sendAsFlow(query: TdApi.Function): Flow = callbackFlow {
213 | client.send(query) {
214 | when (it.constructor) {
215 | TdApi.Error.CONSTRUCTOR -> {
216 | error("")
217 | }
218 | else -> {
219 | offer(it)
220 | }
221 | }
222 | //close()
223 | }
224 | awaitClose { }
225 | }
226 |
227 | inline fun send(query: TdApi.Function): Flow =
228 | sendAsFlow(query).map { it as T }
229 | }
230 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ibashkimi/telegram/data/UserRepository.kt:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram.data
2 |
3 | import kotlinx.coroutines.ExperimentalCoroutinesApi
4 | import kotlinx.coroutines.channels.awaitClose
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.callbackFlow
7 | import org.drinkless.td.libcore.telegram.TdApi
8 | import javax.inject.Inject
9 |
10 | @ExperimentalCoroutinesApi
11 | class UserRepository @Inject constructor(private val client: TelegramClient) {
12 |
13 | fun getUser(userId: Int): Flow = callbackFlow {
14 | client.client.send(TdApi.GetUser(userId)) {
15 | offer(it as TdApi.User)
16 | }
17 | awaitClose { }
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ibashkimi/telegram/data/chats/ChatsPagingSource.kt:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram.data.chats
2 |
3 | import androidx.paging.PagingSource
4 | import androidx.paging.PagingState
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.flow.first
7 | import org.drinkless.td.libcore.telegram.TdApi
8 | import javax.inject.Inject
9 |
10 | @OptIn(ExperimentalCoroutinesApi::class)
11 | class ChatsPagingSource @Inject constructor(
12 | private val chats: ChatsRepository
13 | ) : PagingSource() {
14 |
15 | override suspend fun load(
16 | params: LoadParams
17 | ): LoadResult {
18 | try {
19 | val nextPageNumber = params.key ?: Long.MAX_VALUE
20 | val response = chats.getChats(
21 | nextPageNumber,
22 | params.loadSize
23 | )
24 | val chats = response.first()
25 | return LoadResult.Page(
26 | data = chats,
27 | prevKey = null,
28 | nextKey = chats.lastOrNull()?.positions?.firstOrNull()?.order
29 | )
30 |
31 | } catch (e: Exception) {
32 | return LoadResult.Error(e)
33 | }
34 | }
35 |
36 | override fun getRefreshKey(state: PagingState): Long? {
37 | return null
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ibashkimi/telegram/data/chats/ChatsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram.data.chats
2 |
3 | import androidx.paging.PagingSource
4 | import com.ibashkimi.telegram.data.TelegramClient
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.channels.awaitClose
7 | import kotlinx.coroutines.flow.*
8 | import org.drinkless.td.libcore.telegram.TdApi
9 | import javax.inject.Inject
10 |
11 | @ExperimentalCoroutinesApi
12 | class ChatsRepository @Inject constructor(private val client: TelegramClient) {
13 |
14 | private fun getChatIds(offsetOrder: Long = Long.MAX_VALUE, limit: Int): Flow =
15 | callbackFlow {
16 | client.client.send(TdApi.GetChats(TdApi.ChatListMain(), offsetOrder, 0, limit)) {
17 | when (it.constructor) {
18 | TdApi.Chats.CONSTRUCTOR -> {
19 | offer((it as TdApi.Chats).chatIds)
20 | }
21 | TdApi.Error.CONSTRUCTOR -> {
22 | error("")
23 | }
24 | else -> {
25 | error("")
26 | }
27 | }
28 | //close()
29 | }
30 | awaitClose { }
31 | }
32 |
33 | fun getChats(offsetOrder: Long = Long.MAX_VALUE, limit: Int): Flow> =
34 | getChatIds(offsetOrder, limit)
35 | .map { ids -> ids.map { getChat(it) } }
36 | .flatMapLatest { chatsFlow ->
37 | combine(chatsFlow) { chats ->
38 | chats.toList()
39 | }
40 | }
41 |
42 | fun getChat(chatId: Long): Flow = callbackFlow {
43 | client.client.send(TdApi.GetChat(chatId)) {
44 | when (it.constructor) {
45 | TdApi.Chat.CONSTRUCTOR -> {
46 | offer(it as TdApi.Chat)
47 | }
48 | TdApi.Error.CONSTRUCTOR -> {
49 | error("Something went wrong")
50 | }
51 | else -> {
52 | error("Something went wrong")
53 | }
54 | }
55 | //close()
56 | }
57 | awaitClose { }
58 | }
59 |
60 | fun chatImage(chat: TdApi.Chat): Flow =
61 | chat.photo?.small?.takeIf {
62 | it.local?.isDownloadingCompleted == false
63 | }?.id?.let { fileId ->
64 | client.downloadFile(fileId).map { chat.photo?.small?.local?.path }
65 | } ?: flowOf(chat.photo?.small?.local?.path)
66 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ibashkimi/telegram/data/messages/MessagesPagingSource.kt:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram.data.messages
2 |
3 | import androidx.paging.PagingSource
4 | import androidx.paging.PagingState
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.flow.first
7 | import org.drinkless.td.libcore.telegram.TdApi
8 |
9 | @OptIn(ExperimentalCoroutinesApi::class)
10 | class MessagesPagingSource(
11 | private val chatId: Long,
12 | private val messages: MessagesRepository
13 | ) : PagingSource() {
14 |
15 | override suspend fun load(params: LoadParams): LoadResult {
16 | return try {
17 | val response = messages.getMessages(
18 | chatId = chatId,
19 | fromMessageId = params.key ?: 0,
20 | limit = params.loadSize
21 | )
22 | val messages = response.first()
23 | LoadResult.Page(
24 | data = messages,
25 | prevKey = null,
26 | nextKey = messages.lastOrNull()?.id
27 | )
28 | } catch (e: Exception) {
29 | LoadResult.Error(e)
30 | }
31 | }
32 |
33 | override fun getRefreshKey(state: PagingState): Long? {
34 | return null
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ibashkimi/telegram/data/messages/MessagesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram.data.messages
2 |
3 | import androidx.paging.PagingSource
4 | import com.ibashkimi.telegram.data.TelegramClient
5 | import kotlinx.coroutines.CompletableDeferred
6 | import kotlinx.coroutines.Deferred
7 | import kotlinx.coroutines.ExperimentalCoroutinesApi
8 | import kotlinx.coroutines.channels.awaitClose
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.callbackFlow
11 | import org.drinkless.td.libcore.telegram.TdApi
12 | import javax.inject.Inject
13 |
14 | @OptIn(ExperimentalCoroutinesApi::class)
15 | class MessagesRepository @Inject constructor(val client: TelegramClient) {
16 |
17 | fun getMessages(chatId: Long, fromMessageId: Long, limit: Int): Flow> =
18 | callbackFlow {
19 | client.client.send(TdApi.GetChatHistory(chatId, fromMessageId, 0, limit, false)) {
20 | when (it.constructor) {
21 | TdApi.Messages.CONSTRUCTOR -> {
22 | offer((it as TdApi.Messages).messages.toList())
23 | }
24 | TdApi.Error.CONSTRUCTOR -> {
25 | error("")
26 | }
27 | else -> {
28 | error("")
29 | }
30 | }
31 | }
32 | awaitClose { }
33 | }
34 |
35 | fun getMessagesPaged(chatId: Long): PagingSource =
36 | MessagesPagingSource(chatId, this)
37 |
38 | fun getMessage(chatId: Long, messageId: Long): Flow = callbackFlow {
39 | client.client.send(TdApi.GetMessage(chatId, messageId)) {
40 | when (it.constructor) {
41 | TdApi.Message.CONSTRUCTOR -> {
42 | offer(it as TdApi.Message)
43 | }
44 | TdApi.Error.CONSTRUCTOR -> {
45 | error("Something went wrong")
46 | }
47 | else -> {
48 | error("Something went wrong")
49 | }
50 | }
51 | }
52 | awaitClose { }
53 | }
54 |
55 | fun sendMessage(
56 | chatId: Long,
57 | messageThreadId: Long = 0,
58 | replyToMessageId: Long = 0,
59 | options: TdApi.MessageSendOptions = TdApi.MessageSendOptions(),
60 | inputMessageContent: TdApi.InputMessageContent
61 | ): Deferred = sendMessage(
62 | TdApi.SendMessage(
63 | chatId,
64 | messageThreadId,
65 | replyToMessageId,
66 | options,
67 | null,
68 | inputMessageContent
69 | )
70 | )
71 |
72 | fun sendMessage(sendMessage: TdApi.SendMessage): Deferred {
73 | val result = CompletableDeferred()
74 | client.client.send(sendMessage) {
75 | when (it.constructor) {
76 | TdApi.Message.CONSTRUCTOR -> {
77 | result.complete(it as TdApi.Message)
78 | }
79 | else -> {
80 | result.completeExceptionally(error("Something went wrong"))
81 | }
82 | }
83 | }
84 | return result
85 | }
86 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ibashkimi/telegram/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram.di
2 |
3 | import android.content.Context
4 | import android.os.Build
5 | import com.ibashkimi.telegram.R
6 | import com.ibashkimi.telegram.data.TelegramClient
7 | import com.ibashkimi.telegram.data.UserRepository
8 | import com.ibashkimi.telegram.data.chats.ChatsRepository
9 | import com.ibashkimi.telegram.data.messages.MessagesRepository
10 | import dagger.Module
11 | import dagger.Provides
12 | import dagger.hilt.InstallIn
13 | import dagger.hilt.android.qualifiers.ApplicationContext
14 | import dagger.hilt.components.SingletonComponent
15 | import kotlinx.coroutines.CoroutineScope
16 | import kotlinx.coroutines.Dispatchers
17 | import kotlinx.coroutines.SupervisorJob
18 | import org.drinkless.td.libcore.telegram.TdApi
19 | import java.util.*
20 | import javax.inject.Named
21 | import javax.inject.Singleton
22 |
23 | @Module
24 | @InstallIn(SingletonComponent::class)
25 | object AppModule {
26 |
27 | @Singleton
28 | @Provides
29 | @Named("ioDispatcher")
30 | fun provideIoDispatcher() = Dispatchers.IO
31 |
32 | @Singleton
33 | @Provides
34 | @Named("applicationScope")
35 | fun provideApplicationScope() = CoroutineScope(SupervisorJob())
36 |
37 | @Provides
38 | fun provideTdlibParameters(@ApplicationContext context: Context): TdApi.TdlibParameters {
39 | return TdApi.TdlibParameters().apply {
40 | // Obtain application identifier hash for Telegram API access at https://my.telegram.org
41 | apiId = context.resources.getInteger(R.integer.telegram_api_id)
42 | apiHash = context.getString(R.string.telegram_api_hash)
43 | useMessageDatabase = true
44 | useSecretChats = true
45 | systemLanguageCode = Locale.getDefault().language
46 | databaseDirectory = context.filesDir.absolutePath
47 | deviceModel = Build.MODEL
48 | systemVersion = Build.VERSION.RELEASE
49 | applicationVersion = "0.1"
50 | enableStorageOptimizer = true
51 | }
52 | }
53 |
54 | @Singleton
55 | @Provides
56 | fun provideTelegramClient(parameters: TdApi.TdlibParameters) = TelegramClient(parameters)
57 |
58 | @Provides
59 | fun provideChatsRepository(client: TelegramClient) = ChatsRepository(client)
60 |
61 | @Provides
62 | fun provideMessagesRepository(client: TelegramClient) = MessagesRepository(client)
63 |
64 | @Provides
65 | fun provideUserRepository(client: TelegramClient) = UserRepository(client)
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ibashkimi/telegram/ui/TelegramApp.kt:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram.ui
2 |
3 | import android.app.Activity
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.graphics.toArgb
7 | import androidx.hilt.navigation.compose.hiltNavGraphViewModel
8 | import androidx.navigation.NavHostController
9 | import androidx.navigation.compose.*
10 | import com.ibashkimi.telegram.Screen
11 | import com.ibashkimi.telegram.ui.chat.ChatScreen
12 | import com.ibashkimi.telegram.ui.chat.ChatScreenViewModel
13 | import com.ibashkimi.telegram.ui.createchat.CreateChatScreen
14 | import com.ibashkimi.telegram.ui.home.MainScreen
15 | import com.ibashkimi.telegram.ui.login.LoginScreen
16 | import com.ibashkimi.telegram.ui.theme.TelegramTheme
17 |
18 | @Composable
19 | fun TelegramApp(activity: Activity) {
20 | TelegramTheme {
21 | activity.window.statusBarColor = MaterialTheme.colors.primaryVariant.toArgb()
22 | val navController = rememberNavController()
23 | MainNavHost(navController)
24 | }
25 | }
26 |
27 | @Composable
28 | private fun MainNavHost(navController: NavHostController) {
29 | NavHost(navController, startDestination = Screen.Home.route) {
30 | composable(Screen.Home.route) {
31 | MainScreen(navController = navController, viewModel = hiltNavGraphViewModel(it))
32 | }
33 | composable(Screen.Chat.route) {
34 | val chatId = Screen.Chat.getChatId(it)
35 | val viewModel: ChatScreenViewModel =
36 | navController.hiltNavGraphViewModel(Screen.Chat.route)
37 | viewModel.setChatId(chatId)
38 | ChatScreen(
39 | chatId = chatId,
40 | navController = navController,
41 | viewModel = viewModel
42 | )
43 | }
44 | composable(Screen.CreateChat.route) {
45 | CreateChatScreen(
46 | navigateUp = navController::navigateUp,
47 | viewModel = hiltNavGraphViewModel(it)
48 | )
49 | }
50 | composable(Screen.Login.route) {
51 | LoginScreen(hiltNavGraphViewModel(it)) {
52 | navController.navigate(Screen.Home.route) {
53 | popUpTo(Screen.Login.route) { inclusive = true }
54 | }
55 | }
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ibashkimi/telegram/ui/chat/ChatScreen.kt:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram.ui.chat
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.foundation.shape.CircleShape
8 | import androidx.compose.foundation.shape.RoundedCornerShape
9 | import androidx.compose.material.*
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.ArrowBack
12 | import androidx.compose.material.icons.filled.Gif
13 | import androidx.compose.material.icons.outlined.AttachFile
14 | import androidx.compose.material.icons.outlined.Mic
15 | import androidx.compose.material.icons.outlined.Send
16 | import androidx.compose.runtime.*
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.draw.clip
20 | import androidx.compose.ui.graphics.Color
21 | import androidx.compose.ui.res.stringResource
22 | import androidx.compose.ui.text.input.TextFieldValue
23 | import androidx.compose.ui.unit.dp
24 | import androidx.lifecycle.viewmodel.compose.viewModel
25 | import androidx.navigation.NavController
26 | import androidx.paging.LoadState
27 | import androidx.paging.compose.LazyPagingItems
28 | import androidx.paging.compose.collectAsLazyPagingItems
29 | import androidx.paging.compose.itemsIndexed
30 | import com.ibashkimi.telegram.R
31 | import com.ibashkimi.telegram.data.TelegramClient
32 | import com.ibashkimi.telegram.ui.util.TelegramImage
33 | import kotlinx.coroutines.ExperimentalCoroutinesApi
34 | import kotlinx.coroutines.launch
35 | import org.drinkless.td.libcore.telegram.TdApi
36 |
37 | @OptIn(ExperimentalCoroutinesApi::class)
38 | @Composable
39 | fun ChatScreen(
40 | chatId: Long,
41 | navController: NavController,
42 | modifier: Modifier = Modifier,
43 | viewModel: ChatScreenViewModel = viewModel()
44 | ) {
45 | LaunchedEffect(chatId) {
46 | viewModel.setChatId(chatId)
47 | }
48 | val chat = viewModel.chat.collectAsState(null)
49 | Scaffold(
50 | modifier = modifier,
51 | topBar = {
52 | TopAppBar(
53 | title = { Text(chat.value?.title ?: "", maxLines = 1) },
54 | navigationIcon = {
55 | IconButton(onClick = { navController.navigateUp() }) {
56 | Icon(
57 | imageVector = Icons.Default.ArrowBack,
58 | contentDescription = null
59 | )
60 | }
61 | })
62 | },
63 | content = {
64 | ChatContent(viewModel)
65 | }
66 | )
67 | }
68 |
69 | @Composable
70 | fun ChatContent(viewModel: ChatScreenViewModel, modifier: Modifier = Modifier) {
71 | val history = viewModel.messagesPaged.collectAsLazyPagingItems()
72 |
73 | Column(modifier = modifier.fillMaxWidth()) {
74 | ChatHistory(
75 | client = viewModel.client,
76 | messages = history,
77 | modifier = Modifier
78 | .fillMaxWidth()
79 | .weight(1.0f)
80 | )
81 | val input = remember { mutableStateOf(TextFieldValue("")) }
82 | val scope = rememberCoroutineScope()
83 | MessageInput(
84 | input = input,
85 | insertGif = {
86 | // TODO
87 | }, attachFile = {
88 | // todo
89 | }, sendMessage = {
90 | scope.launch {
91 | viewModel.sendMessage(
92 | inputMessageContent = TdApi.InputMessageText(
93 | TdApi.FormattedText(
94 | it,
95 | emptyArray()
96 | ), false, false
97 | )
98 | ).await()
99 | input.value = TextFieldValue()
100 | history.refresh()
101 | }
102 | })
103 | }
104 | }
105 |
106 | @Composable
107 | fun ChatLoading(modifier: Modifier = Modifier) {
108 | Text(
109 | text = stringResource(R.string.loading),
110 | style = MaterialTheme.typography.h5,
111 | modifier = modifier
112 | .fillMaxSize()
113 | .wrapContentSize(Alignment.Center)
114 | )
115 | }
116 |
117 | @Composable
118 | fun ChatHistory(
119 | client: TelegramClient,
120 | messages: LazyPagingItems,
121 | modifier: Modifier = Modifier
122 | ) {
123 | LazyColumn(modifier = modifier, reverseLayout = true) {
124 | when {
125 | messages.loadState.refresh is LoadState.Loading -> {
126 | item {
127 | ChatLoading()
128 | }
129 | }
130 | messages.loadState.refresh is LoadState.Error -> {
131 | item {
132 | Text(
133 | text = "Cannot load messages",
134 | style = MaterialTheme.typography.h5,
135 | modifier = modifier
136 | .fillMaxSize()
137 | .wrapContentSize(Alignment.Center)
138 | )
139 | }
140 | }
141 | messages.loadState.refresh is LoadState.NotLoading && messages.itemCount == 0 -> {
142 | item {
143 | Text("Empty")
144 | }
145 | }
146 | }
147 | itemsIndexed(messages) { i, message ->
148 | message?.let {
149 | val userId = (message.sender as TdApi.MessageSenderUser).userId
150 | val previousMessageUserId =
151 | if (i > 0) (messages[i - 1]?.sender as TdApi.MessageSenderUser?)?.userId else null
152 | MessageItem(
153 | isSameUserFromPreviousMessage = userId == previousMessageUserId,
154 | client,
155 | it
156 | )
157 | }
158 | }
159 | }
160 | }
161 |
162 | @Composable
163 | private fun MessageItem(
164 | isSameUserFromPreviousMessage: Boolean,
165 | client: TelegramClient,
166 | message: TdApi.Message,
167 | modifier: Modifier = Modifier
168 | ) {
169 | if (message.isOutgoing) {
170 | Box(
171 | Modifier
172 | .clickable(onClick = {})
173 | .fillMaxWidth(),
174 | contentAlignment = Alignment.BottomEnd
175 | ) {
176 | MessageItemCard(modifier = Modifier.padding(8.dp, 4.dp, 8.dp, 4.dp)) {
177 | MessageItemContent(
178 | client,
179 | message,
180 | modifier = Modifier
181 | .background(Color.Green.copy(alpha = 0.2f))
182 | .padding(8.dp)
183 | )
184 | }
185 | }
186 | } else {
187 | Row(
188 | verticalAlignment = Alignment.Bottom,
189 | modifier = Modifier.clickable(onClick = {}) then modifier.fillMaxWidth()
190 | ) {
191 | if (!isSameUserFromPreviousMessage) {
192 | ChatUserIcon(
193 | client,
194 | (message.sender as TdApi.MessageSenderUser).userId,
195 | Modifier
196 | .padding(8.dp)
197 | .clip(shape = CircleShape)
198 | .size(42.dp)
199 | )
200 | } else {
201 | Box(
202 | Modifier
203 | .padding(8.dp)
204 | .size(42.dp))
205 | }
206 | MessageItemCard(modifier = Modifier.padding(0.dp, 4.dp, 8.dp, 4.dp)) {
207 | MessageItemContent(
208 | client,
209 | message,
210 | modifier = Modifier.padding(8.dp)
211 | )
212 | }
213 | }
214 | }
215 | }
216 |
217 | @Composable
218 | private fun MessageItemCard(
219 | modifier: Modifier = Modifier,
220 | content: @Composable () -> Unit
221 | ) = Card(
222 | elevation = 2.dp,
223 | shape = RoundedCornerShape(8.dp),
224 | modifier = modifier,
225 | content = content
226 | )
227 |
228 | @Composable
229 | private fun MessageItemContent(
230 | client: TelegramClient,
231 | message: TdApi.Message,
232 | modifier: Modifier = Modifier
233 | ) {
234 | when (message.content) {
235 | is TdApi.MessageText -> TextMessage(message, modifier)
236 | is TdApi.MessageVideo -> VideoMessage(message, modifier)
237 | is TdApi.MessageCall -> CallMessage(message, modifier)
238 | is TdApi.MessageAudio -> AudioMessage(message, modifier)
239 | is TdApi.MessageSticker -> StickerMessage(client, message, modifier)
240 | is TdApi.MessageAnimation -> AnimationMessage(client, message, modifier)
241 | is TdApi.MessagePhoto -> PhotoMessage(client, message, Modifier)
242 | is TdApi.MessageVideoNote -> VideoNoteMessage(client, message, modifier)
243 | is TdApi.MessageVoiceNote -> VoiceNoteMessage(message, modifier)
244 | else -> UnsupportedMessage()
245 | }
246 | }
247 |
248 | @Composable
249 | private fun ChatUserIcon(client: TelegramClient, userId: Int, modifier: Modifier) {
250 | val user = client.send(TdApi.GetUser(userId)).collectAsState(initial = null).value
251 | TelegramImage(client, user?.profilePhoto?.small, modifier = modifier)
252 | }
253 |
254 | @Composable
255 | fun MessageInput(
256 | modifier: Modifier = Modifier,
257 | input: MutableState = remember { mutableStateOf(TextFieldValue("")) },
258 | insertGif: () -> Unit = {},
259 | attachFile: () -> Unit = {},
260 | sendMessage: (String) -> Unit = {}
261 | ) {
262 | Surface(modifier, color = MaterialTheme.colors.surface, elevation = 6.dp) {
263 | TextField(
264 | value = input.value,
265 | modifier = Modifier.fillMaxWidth(),
266 | onValueChange = { input.value = it },
267 | textStyle = MaterialTheme.typography.body1,
268 | placeholder = {
269 | Text("Message")
270 | },
271 | leadingIcon = {
272 | IconButton(onClick = insertGif) {
273 | Icon(
274 | imageVector = Icons.Default.Gif,
275 | contentDescription = null
276 | )
277 | }
278 | },
279 | trailingIcon = {
280 | if (input.value.text.isEmpty()) {
281 | Row {
282 | IconButton(onClick = attachFile) {
283 | Icon(
284 | imageVector = Icons.Outlined.AttachFile,
285 | contentDescription = null
286 | )
287 | }
288 | IconButton(onClick = { }) {
289 | Icon(
290 | imageVector = Icons.Outlined.Mic,
291 | contentDescription = null
292 | )
293 | }
294 | }
295 | } else {
296 | IconButton(onClick = { sendMessage(input.value.text) }) {
297 | Icon(
298 | imageVector = Icons.Outlined.Send,
299 | contentDescription = null
300 | )
301 | }
302 | }
303 | },
304 | colors = TextFieldDefaults.textFieldColors(backgroundColor = MaterialTheme.colors.surface)
305 | )
306 | }
307 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ibashkimi/telegram/ui/chat/ChatScreenViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram.ui.chat
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import androidx.paging.Pager
6 | import androidx.paging.PagingConfig
7 | import androidx.paging.PagingData
8 | import androidx.paging.cachedIn
9 | import com.ibashkimi.telegram.data.TelegramClient
10 | import com.ibashkimi.telegram.data.chats.ChatsRepository
11 | import com.ibashkimi.telegram.data.messages.MessagesRepository
12 | import dagger.hilt.android.lifecycle.HiltViewModel
13 | import kotlinx.coroutines.Deferred
14 | import kotlinx.coroutines.flow.Flow
15 | import org.drinkless.td.libcore.telegram.TdApi
16 | import javax.inject.Inject
17 |
18 | @HiltViewModel
19 | class ChatScreenViewModel @Inject constructor(
20 | // TODO: inject chatId https://github.com/google/dagger/issues/2287
21 | val client: TelegramClient,
22 | val chatsRepository: ChatsRepository,
23 | val messagesRepository: MessagesRepository
24 | ) : ViewModel() {
25 |
26 | private var chatId: Long = -1
27 |
28 | lateinit var chat: Flow
29 | private set
30 |
31 | lateinit var messagesPaged: Flow>
32 | private set
33 |
34 | fun setChatId(chatId: Long) {
35 | this.chatId = chatId
36 | this.chat = chatsRepository.getChat(chatId)
37 | this.messagesPaged = Pager(PagingConfig(pageSize = 30)) {
38 | messagesRepository.getMessagesPaged(chatId)
39 | }.flow.cachedIn(viewModelScope)
40 | }
41 |
42 | fun sendMessage(
43 | messageThreadId: Long = 0,
44 | replyToMessageId: Long = 0,
45 | options: TdApi.MessageSendOptions = TdApi.MessageSendOptions(),
46 | inputMessageContent: TdApi.InputMessageContent
47 | ): Deferred {
48 | return messagesRepository.sendMessage(
49 | chatId, messageThreadId, replyToMessageId, options, inputMessageContent
50 | )
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ibashkimi/telegram/ui/chat/MessageItem.kt:
--------------------------------------------------------------------------------
1 | package com.ibashkimi.telegram.ui.chat
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.material.Icon
5 | import androidx.compose.material.MaterialTheme
6 | import androidx.compose.material.Text
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.outlined.Call
9 | import androidx.compose.material.icons.outlined.Done
10 | import androidx.compose.material.icons.outlined.Pending
11 | import androidx.compose.material.icons.outlined.SyncProblem
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.collectAsState
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.draw.alpha
17 | import androidx.compose.ui.platform.LocalDensity
18 | import androidx.compose.ui.unit.Dp
19 | import androidx.compose.ui.unit.dp
20 | import androidx.compose.ui.unit.min
21 | import com.google.accompanist.coil.CoilImage
22 | import com.ibashkimi.telegram.data.TelegramClient
23 | import com.ibashkimi.telegram.ui.util.TelegramImage
24 | import org.drinkless.td.libcore.telegram.TdApi
25 | import java.io.File
26 | import java.util.*
27 |
28 | @Composable
29 | fun TextMessage(message: TdApi.Message, modifier: Modifier = Modifier) {
30 | Column(modifier = modifier, horizontalAlignment = Alignment.End) {
31 | TextMessage(message.content as TdApi.MessageText)
32 | MessageStatus(message)
33 | }
34 | }
35 |
36 | @Composable
37 | private fun TextMessage(content: TdApi.MessageText, modifier: Modifier = Modifier) {
38 | Text(text = content.text.text, modifier = modifier)
39 | }
40 |
41 | @Composable
42 | fun AudioMessage(message: TdApi.Message, modifier: Modifier = Modifier) {
43 | val content = message.content as TdApi.MessageAudio
44 | Column(modifier = modifier, horizontalAlignment = Alignment.End) {
45 | Text(text = "Audio ${content.audio.duration}", modifier = modifier)
46 | content.caption.text.takeIf { it.isNotBlank() }?.let {
47 | Text(it)
48 | }
49 | MessageStatus(message)
50 | }
51 | }
52 |
53 | @Composable
54 | fun VideoMessage(message: TdApi.Message, modifier: Modifier = Modifier) {
55 | val content = message.content as TdApi.MessageVideo
56 | Column(modifier = modifier, horizontalAlignment = Alignment.End) {
57 | Text(text = "Video ${content.video.duration}", modifier = modifier)
58 | content.caption.text.takeIf { it.isNotBlank() }?.let {
59 | Text(it)
60 | }
61 | MessageStatus(message)
62 | }
63 | }
64 |
65 | @Composable
66 | fun StickerMessage(
67 | client: TelegramClient,
68 | message: TdApi.Message,
69 | modifier: Modifier = Modifier
70 | ) {
71 | Column(modifier = modifier, horizontalAlignment = Alignment.End) {
72 | StickerMessage(client, message.content as TdApi.MessageSticker)
73 | MessageStatus(message)
74 | }
75 | }
76 |
77 | @Composable
78 | private fun StickerMessage(
79 | client: TelegramClient,
80 | content: TdApi.MessageSticker,
81 | modifier: Modifier = Modifier
82 | ) {
83 | if (content.sticker.isAnimated) {
84 | Text(text = " ${content.sticker.emoji}", modifier = modifier)
85 | } else {
86 | Box(contentAlignment = Alignment.BottomEnd) {
87 | TelegramImage(client = client, file = content.sticker.sticker)
88 | content.sticker.emoji.takeIf { it.isNotBlank() }?.let {
89 | Text(text = it, modifier = modifier)
90 | }
91 | }
92 | }
93 | }
94 |
95 | @Composable
96 | fun AnimationMessage(
97 | client: TelegramClient,
98 | message: TdApi.Message,
99 | modifier: Modifier = Modifier
100 | ) {
101 | val content = message.content as TdApi.MessageAnimation
102 | val path =
103 | client.downloadableFile(content.animation.animation).collectAsState(initial = null)
104 | Column {
105 | path.value?.let { filePath ->
106 | CoilImage(data = File(filePath), modifier = Modifier.size(56.dp)) {
107 |
108 | }
109 | } ?: Text(text = "path null", modifier = modifier)
110 | Text(text = "path: ${path.value}")
111 | }
112 | }
113 |
114 | @Composable
115 | fun CallMessage(message: TdApi.Message, modifier: Modifier = Modifier) {
116 | Column(modifier = modifier, horizontalAlignment = Alignment.End) {
117 | CallMessage(message.content as TdApi.MessageCall)
118 | MessageStatus(message)
119 | }
120 | }
121 |
122 | @Composable
123 | private fun CallMessage(content: TdApi.MessageCall, modifier: Modifier = Modifier) {
124 | val msg = when (content.discardReason) {
125 | is TdApi.CallDiscardReasonHungUp -> {
126 | "Incoming call"
127 | }
128 | is TdApi.CallDiscardReasonDeclined -> {
129 | "Declined call"
130 | }
131 | is TdApi.CallDiscardReasonDisconnected -> {
132 | "Call disconnected"
133 | }
134 | is TdApi.CallDiscardReasonMissed -> {
135 | "Missed call"
136 | }
137 | is TdApi.CallDiscardReasonEmpty -> {
138 | "Call: Unknown state"
139 | }
140 | else -> "Call: Unknown state"
141 | }
142 | Row(modifier, verticalAlignment = Alignment.CenterVertically) {
143 | Text(text = msg, modifier = modifier)
144 | Icon(
145 | imageVector = Icons.Outlined.Call,
146 | contentDescription = null,
147 | modifier = Modifier
148 | .padding(8.dp)
149 | .size(18.dp)
150 | )
151 | }
152 | }
153 |
154 | @Composable
155 | fun PhotoMessage(client: TelegramClient, message: TdApi.Message, modifier: Modifier = Modifier) {
156 | Column(modifier = modifier, horizontalAlignment = Alignment.End) {
157 | PhotoMessage(client, message.content as TdApi.MessagePhoto)
158 | MessageStatus(message, Modifier.padding(4.dp))
159 | }
160 | /*Box(modifier, contentAlignment = Alignment.BottomEnd) {
161 | PhotoMessage(client, message.content as TdApi.MessagePhoto)
162 | MessageStatus(message = message, modifier = Modifier.padding(8.dp).background(Color.Magenta))
163 | }*/
164 | }
165 |
166 | @Composable
167 | fun VideoNoteMessage(
168 | client: TelegramClient,
169 | message: TdApi.Message,
170 | modifier: Modifier = Modifier
171 | ) {
172 | Column(modifier = modifier, horizontalAlignment = Alignment.End) {
173 | Text("